diff --git a/README.md b/README.md index 0fc0e205..7071ff38 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ const server = http.createServer(app).listen(); async function rpc() { const { text } = await ModuleRpcProtocolClient.getRpcClient(helloServiceDefinition, { remoteAddress: `http://localhost:${server.address().port}` - }).nice().getHello({ language: 'Spanish' }); + }).getHello({ language: 'Spanish' }); // (Notice that, with TypeScript typing, it is not possible to mess up the // type of the request: for instance, `.getHello({ lang: 'Spanish' })` // will error.) diff --git a/docs/primer.md b/docs/primer.md index c3e78048..498a0cc5 100644 --- a/docs/primer.md +++ b/docs/primer.md @@ -36,7 +36,7 @@ const server = http.createServer(app).listen(); async function rpc() { const { text } = await ModuleRpcProtocolClient.getRpcClient(helloServiceDefinition, { remoteAddress: `http://localhost:${server.address().port}` - }).nice().getHello({ language: 'Spanish' }); + }).getHello({ language: 'Spanish' }); // (Notice that, with TypeScript typing, it is not possible to mess up the // type of the request: for instance, `.getHello({ lang: 'Spanish' })` // will error.) diff --git a/package.json b/package.json index ad7647d3..a893b1d1 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "type-zoo": "^1.2.1", "typedoc": "^0.13.0", "typedoc-plugin-external-module-name": "^1.1.3", - "typescript": "3.2.2" + "typescript": "3.4.1" }, "scripts": { "clean": "rimraf lib dist es typedoc", diff --git a/site/landing/index.md b/site/landing/index.md index 7ac2417d..2a5ede3c 100644 --- a/site/landing/index.md +++ b/site/landing/index.md @@ -65,7 +65,7 @@ http.createServer(app).listen(3000);
{% highlight TypeScript %} const { text } = await ModuleRpcProtocolClient.getRpcClient(helloService, { remoteAddress: 'http://localhost:3000' -}).nice().getHello({ language: 'Spanish' }); +}).getHello({ language: 'Spanish' }); {% endhighlight %}
diff --git a/src/client/__tests__/service.test.ts b/src/client/__tests__/service.test.ts index 88190415..10d943f0 100644 --- a/src/client/__tests__/service.test.ts +++ b/src/client/__tests__/service.test.ts @@ -5,7 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ -import { Service, ServiceRetrier } from '../service'; +import { Service, ServiceRetrier, serviceInstance } from '../service'; import * as sinon from 'sinon'; import { Stream } from '../stream'; import { EventEmitter } from 'events'; @@ -179,8 +179,8 @@ describe('rpc_ts', () => { }); }); - describe('nice', () => { - it('unary methods can be called', async () => { + describe('methodMap', () => { + specify('unary methods can be called', async () => { const mockStream = new MockStream(); const streamProducer = sinon.stub().returns(mockStream); const service = new Service< @@ -188,7 +188,7 @@ describe('rpc_ts', () => { ResponseContext >(testServiceDefinition, streamProducer); const request = { foo: 'bar' }; - const promise = service.nice().unary(request); + const promise = service.methodMap().unary(request); const message = { response: { value: 10 }, @@ -201,59 +201,74 @@ describe('rpc_ts', () => { expect(streamProducer.calledOnce).to.be.true; expect(streamProducer.args[0]).to.deep.equal(['unary', request]); }); - }); - it('server streams can be called', async () => { - const mockStream = new MockStream(); - const streamProducer = sinon.stub().returns(mockStream); - const service = new Service< - typeof testServiceDefinition, - ResponseContext - >(testServiceDefinition, streamProducer); - const request = { foo: 'bar' }; - const stream = service.nice().stream(request); + specify('server streams can be called', async () => { + const mockStream = new MockStream(); + const streamProducer = sinon.stub().returns(mockStream); + const service = new Service< + typeof testServiceDefinition, + ResponseContext + >(testServiceDefinition, streamProducer); + const request = { foo: 'bar' }; + const stream = service.methodMap().stream(request); - const message1 = { - response: { value: 10 }, - responseContext: { qux: 'wobble' }, - }; - await new Promise((accept, reject) => { - stream.on('message', message => { - try { - expect(message).to.deep.equal({ value: 10 }); - accept(); - } catch (err) { - reject(err); - } + const message1 = { + response: { value: 10 }, + responseContext: { qux: 'wobble' }, + }; + await new Promise((accept, reject) => { + stream.on('message', message => { + try { + expect(message).to.deep.equal({ value: 10 }); + accept(); + } catch (err) { + reject(err); + } + }); + mockStream.emit('message', message1); }); - mockStream.emit('message', message1); - }); - const message2 = { - response: { value: 20 }, - responseContext: { qux: 'wobble2' }, - }; - await new Promise((accept, reject) => { - stream.on('message', message => { - try { - expect(message).to.deep.equal({ value: 20 }); - accept(); - } catch (err) { - reject(err); - } + const message2 = { + response: { value: 20 }, + responseContext: { qux: 'wobble2' }, + }; + await new Promise((accept, reject) => { + stream.on('message', message => { + try { + expect(message).to.deep.equal({ value: 20 }); + accept(); + } catch (err) { + reject(err); + } + }); + mockStream.emit('message', message2); }); - mockStream.emit('message', message2); - }); - await new Promise(accept => { - stream.on('complete', () => { - accept(); + await new Promise(accept => { + stream.on('complete', () => { + accept(); + }); + mockStream.emit('complete'); }); - mockStream.emit('complete'); + + expect(streamProducer.calledOnce).to.be.true; + expect(streamProducer.args[0]).to.deep.equal(['stream', request]); }); - expect(streamProducer.calledOnce).to.be.true; - expect(streamProducer.args[0]).to.deep.equal(['stream', request]); + specify( + 'serviceInstance(methodMap) gives the original, full service', + async () => { + const mockStream = new MockStream(); + const streamProducer = sinon.stub().returns(mockStream); + const service = new Service< + typeof testServiceDefinition, + ResponseContext + >(testServiceDefinition, streamProducer); + + const methodMap = service.methodMap(); + expect(serviceInstance(methodMap)).to.equal(service); + }, + ); }); }); }); diff --git a/src/client/client.ts b/src/client/client.ts index e1cb2cfe..fb759982 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -16,7 +16,7 @@ export { ClientContextConnector } from './context_connector'; export * from './errors'; export { retryStream } from './stream_retrier'; -export { Service, NiceService } from './service'; +export { Service, ServiceMethodMap } from './service'; export { StreamProducer, Stream, diff --git a/src/client/service.ts b/src/client/service.ts index 5931acca..c88c76f9 100644 --- a/src/client/service.ts +++ b/src/client/service.ts @@ -138,10 +138,10 @@ export class Service< } /** - * Return a "nice" service interface with which it is possible to call RPCs + * Return a "method map" service interface with which it is possible to call RPCs * as "normal" JavaScript functions. * - * @example ```TypeScript + * @example ```Typescript * // Before * const service: Service<...> = ...; * service.stream('serverStream', { foo: 'bar' }) @@ -150,15 +150,15 @@ export class Service< * const { response, responseContext } = await service.call('unaryMethod', { foo: 'bar' }); * * // After - * const niceService = service.nice(); - * niceService.serverStream({ foo: 'bar' }) + * const methodMap = service.methodMap(); + * methodMap.serverStream({ foo: 'bar' }) * .on('message', response => { ... }) * .start(); - * const response = await niceService.unaryMethod({ foo: 'bar' }); + * const response = await methodMap.unaryMethod({ foo: 'bar' }); * ``` */ - nice(): NiceService { - return mapValuesWithStringKeys( + methodMap(): ServiceMethodMap { + const methods = mapValuesWithStringKeys( this.serviceDefinition, (methodDefinition, method) => { if ( @@ -180,6 +180,10 @@ export class Service< } }, ) as any; + return { + ...methods, + [serviceKey]: this, + }; } } @@ -198,12 +202,23 @@ export interface ResponseWithContext< } /** - * "Nice" service derived from a service definition. + * Symbol used as a property name on a [[ServiceMethodMap]] to access the + * full service interface from a "method map" service interface. * - * @see [[Service.nice]] + * This symbol is private to this module as [[serviceInstance]] should + * be used externally instead of directly accessing the service instance + * using the symbol. */ -export type NiceService< - serviceDefinition extends ModuleRpcCommon.ServiceDefinition +const serviceKey = Symbol('serviceKey'); + +/** + * "Method map" service interface derived from a service definition. + * + * @see [[Service.methodMap]] + */ +export type ServiceMethodMap< + serviceDefinition extends ModuleRpcCommon.ServiceDefinition, + ResponseContext = any > = { [method in ModuleRpcCommon.MethodsFor< serviceDefinition @@ -212,9 +227,30 @@ export type NiceService< } ? ServerStreamMethod : UnaryMethod +} & { + [serviceKey]: Service; }; -/** A unary method typed as part of a "nice" service. */ +/** + * Get the full service instance from a "method map" service interface. + * + * Implementation detail: The service instance is stored in a "method map" through a + * symbol-named property. + * + * @example ```Typescript + * const service: Service<...> = ...; + * const methodMap = service.methodMap(); + * // serviceInstance(methodMap) === service + * ``` + */ +export function serviceInstance< + serviceDefinition extends ModuleRpcCommon.ServiceDefinition, + ResponseContext = any +>(methodMap: ServiceMethodMap) { + return methodMap[serviceKey]; +} + +/** A unary method typed as part of a "method map" service interface. */ export interface UnaryMethod< serviceDefinition extends ModuleRpcCommon.ServiceDefinition, method extends ModuleRpcCommon.MethodsFor @@ -224,7 +260,7 @@ export interface UnaryMethod< >; } -/** A server-stream method typed as part of a "nice" service. */ +/** A server-stream method typed as part of a "method map" service interface. */ export interface ServerStreamMethod< serviceDefinition extends ModuleRpcCommon.ServiceDefinition, method extends ModuleRpcCommon.MethodsFor diff --git a/src/examples/context/index.ts b/src/examples/context/index.ts index 4332340f..2a04e189 100644 --- a/src/examples/context/index.ts +++ b/src/examples/context/index.ts @@ -66,7 +66,7 @@ async function clientInteraction(remoteAddress: string) { remoteAddress, clientContextConnector: new AuthClientContextConnector('u1'), }, - ).nice(); + ); const balanceResponseBefore = await client.getBalance({}); console.log('Balance is', balanceResponseBefore.value); @@ -88,7 +88,7 @@ async function clientInteraction(remoteAddress: string) { remoteAddress, clientContextConnector: new AuthClientContextConnector('u2'), }, - ).nice(); + ); try { await unauthenticatedClient.getBalance({}); diff --git a/src/examples/server_stream/index.ts b/src/examples/server_stream/index.ts index 2787ff19..9d94c93c 100644 --- a/src/examples/server_stream/index.ts +++ b/src/examples/server_stream/index.ts @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ import * as express from 'express'; -import { getGrpcWebClient } from '../../protocol/grpc_web/client/client'; import { numberServiceDefinition, NumberService } from './service'; import * as http from 'http'; import { getNumberHandler } from './handler'; @@ -14,8 +13,8 @@ import { ModuleRpcClient } from '../../client'; import { ModuleRpcCommon } from '../../common'; import { AssertionError } from 'assert'; import { ModuleRpcContextServer } from '../../context/server'; -import { ModuleRpcContextClient } from '../../context/client'; import { ModuleRpcProtocolServer } from '../../protocol/server'; +import { ModuleRpcProtocolClient } from '../../protocol/client'; main().catch( /* istanbul ignore next */ @@ -59,11 +58,9 @@ function setupServer() { } async function clientInteraction(remoteAddress: string) { - const client = getGrpcWebClient( - numberServiceDefinition, - new ModuleRpcContextClient.EmptyClientContextConnector(), - { remoteAddress }, - ).nice(); + const client = ModuleRpcProtocolClient.getRpcClient(numberServiceDefinition, { + remoteAddress, + }); // We call our unary method const { value } = await client.increment({ value: 10 }); @@ -88,7 +85,7 @@ async function clientInteraction(remoteAddress: string) { } function streamNumbers( - client: ModuleRpcClient.NiceService, + client: ModuleRpcClient.ServiceMethodMap, ): ModuleRpcClient.Stream< ModuleRpcCommon.ResponseFor > { diff --git a/src/protocol/client/client.ts b/src/protocol/client/client.ts index fa48df42..56a24628 100644 --- a/src/protocol/client/client.ts +++ b/src/protocol/client/client.ts @@ -33,7 +33,7 @@ export function getRpcClient< >( serviceDefinition: serviceDefinition, options: RpcClientOptions, -): ModuleRpcClient.Service; +): ModuleRpcClient.ServiceMethodMap; export function getRpcClient< serviceDefinition extends ModuleRpcCommon.ServiceDefinition, ResponseContext @@ -44,7 +44,7 @@ export function getRpcClient< ResponseContext >; }, -): ModuleRpcClient.Service; +): ModuleRpcClient.ServiceMethodMap; export function getRpcClient< serviceDefinition extends ModuleRpcCommon.ServiceDefinition, ResponseContext @@ -55,15 +55,15 @@ export function getRpcClient< ResponseContext >; }, -): ModuleRpcClient.Service { +): ModuleRpcClient.ServiceMethodMap { const clientContextConnector = options.clientContextConnector || new ModuleRpcContextClient.EmptyClientContextConnector(); - return ModuleRpcProtocolGrpcWebClient.getGrpcWebClient( + return (ModuleRpcProtocolGrpcWebClient.getGrpcWebClient( serviceDefinition, clientContextConnector, { remoteAddress: options.remoteAddress, }, - ) as ModuleRpcClient.Service; + ) as ModuleRpcClient.Service).methodMap(); } diff --git a/src/protocol/grpc_web/__tests__/client_server.it.ts b/src/protocol/grpc_web/__tests__/client_server.it.ts index 95a1f664..b99be769 100644 --- a/src/protocol/grpc_web/__tests__/client_server.it.ts +++ b/src/protocol/grpc_web/__tests__/client_server.it.ts @@ -266,7 +266,7 @@ describe('rpc_ts', () => { { remoteAddress: `http://example.test` }, ); try { - await client.nice().unary({}); + await client.methodMap().unary({}); throw new AssertionError({ message: 'expected an Error to be thrown', }); @@ -307,7 +307,7 @@ describe('rpc_ts', () => { codec: new InvalidCodec(), }, ); - await client.nice().unary({}); + await client.methodMap().unary({}); throw new AssertionError({ message: 'expected an Error to be thrown', }); @@ -475,7 +475,7 @@ describe('rpc_ts', () => { new ModuleRpcContextClient.EmptyClientContextConnector(), { remoteAddress: `http://localhost:${serverPort}/api` }, ); - await client.nice().unary({}); + await client.methodMap().unary({}); throw new AssertionError({ message: 'expected an Error to be thrown', }); @@ -523,7 +523,9 @@ describe('rpc_ts', () => { new ModuleRpcContextClient.EmptyClientContextConnector(), { remoteAddress: `http://localhost:${serverPort}/api` }, ); - await ModuleRpcClient.streamAsPromise(client.nice().serverStream({})); + await ModuleRpcClient.streamAsPromise( + client.methodMap().serverStream({}), + ); throw new AssertionError({ message: 'expected an Error to be thrown', }); diff --git a/src/protocol/grpc_web/client/client.ts b/src/protocol/grpc_web/client/client.ts index e13cabee..226a29a6 100644 --- a/src/protocol/grpc_web/client/client.ts +++ b/src/protocol/grpc_web/client/client.ts @@ -52,8 +52,8 @@ export interface GrpcWebClientOptions { /** * Returns an RPC client for the gRPC-Web protocol. * - * @see [[getRpcClient]] offers a more generic interface and is enough - * for most use cases. + * @see [[getRpcClient]] offers a more generic interface with a more intuitive "method map" + * service interface and is enough for most use cases. * * @param serviceDefinition The definition of the service for which to implement a client. * @param clientContextConnector The context connector to use to inject metadata (such diff --git a/src/protocol/grpc_web/private/grpc.ts b/src/protocol/grpc_web/private/grpc.ts index c8a826b9..bde1725e 100644 --- a/src/protocol/grpc_web/private/grpc.ts +++ b/src/protocol/grpc_web/private/grpc.ts @@ -82,7 +82,7 @@ export const errorTypesToGrpcStatuses: { /** Conversion table from gRPC statuses to error types. */ export const grpcStatusesToErrorTypes = _.fromPairs( _.map(errorTypesToGrpcStatuses, (status, errorType) => [status, errorType]), -); +) as { [errorCode: number]: ModuleRpcCommon.RpcErrorType }; /** Get the error, or null in case the RPC succeeded, for the RPC call metadata. */ export function getGrpcWebErrorFromMetadata( diff --git a/src/protocol/mock/__tests__/mock.test.ts b/src/protocol/mock/__tests__/mock.test.ts index c4432867..59dc8d8e 100644 --- a/src/protocol/mock/__tests__/mock.test.ts +++ b/src/protocol/mock/__tests__/mock.test.ts @@ -26,7 +26,7 @@ describe('rpc_ts', () => { { response: { bar: 1 } }, ]) as ModuleRpcClient.Stream; }); - const { bar } = await client.nice().foo({}); + const { bar } = await client.methodMap().foo({}); expect(bar).to.equal(1); }); }); diff --git a/src/protocol/mock/mock.ts b/src/protocol/mock/mock.ts index 82b9fc9f..9f2ae5c6 100644 --- a/src/protocol/mock/mock.ts +++ b/src/protocol/mock/mock.ts @@ -31,7 +31,7 @@ import { ModuleRpcCommon } from '../../common'; * { response: { bar: 1 } }, * ]) as ModuleRpcClient.Stream; * }); - * const { bar } = await client.nice().foo({}); + * const { bar } = await client.methodMap().foo({}); * expect(bar).to.equal(1); * ``` */ diff --git a/yarn.lock b/yarn.lock index 3b248c5f..6aa69106 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3313,10 +3313,10 @@ typescript@3.1.x: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68" integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA== -typescript@3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.2.tgz#fe8101c46aa123f8353523ebdcf5730c2ae493e5" - integrity sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg== +typescript@3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6" + integrity sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q== uglify-js@^3.1.4: version "3.4.9"