Skip to content

Commit db0cefb

Browse files
fix(microservices): fix client-side grpc streaming
1 parent dd8380e commit db0cefb

5 files changed

Lines changed: 162 additions & 33 deletions

File tree

integration/microservices/e2e/orders-grpc.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,52 @@ describe('Advanced GRPC transport', () => {
166166
setTimeout(() => resolve(), 1000);
167167
});
168168
});
169+
170+
it('GRPC Sending Stream and receiving a single message from RX handler', async () => {
171+
const callHandler = client.streamReq((err, res) => {
172+
if (err) {
173+
throw err;
174+
}
175+
expect(res).to.eql({
176+
id: 1,
177+
itemTypes: [1],
178+
shipmentType: {
179+
from: 'test',
180+
to: 'test1',
181+
carrier: 'test-carrier',
182+
},
183+
});
184+
});
185+
186+
return new Promise((resolve, reject) => {
187+
callHandler.write({
188+
id: 1,
189+
});
190+
setTimeout(() => resolve(), 1000);
191+
});
192+
});
193+
194+
it('GRPC Sending Stream and receiving a single message from Call handler', async () => {
195+
const callHandler = client.streamReqCall((err, res) => {
196+
if (err) {
197+
throw err;
198+
}
199+
expect(res).to.eql({
200+
id: 1,
201+
itemTypes: [1],
202+
shipmentType: {
203+
from: 'test',
204+
to: 'test1',
205+
carrier: 'test-carrier',
206+
},
207+
})
208+
});
209+
210+
return new Promise((resolve, reject) => {
211+
callHandler.write({
212+
id: 1,
213+
});
214+
setTimeout(() => resolve(), 1000);
215+
});
216+
});
169217
});

integration/microservices/src/grpc-advanced/advanced.grpc.controller.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,44 @@ export class AdvancedGrpcController {
9797
});
9898
});
9999
}
100+
101+
@GrpcStreamMethod('orders.OrderService')
102+
async streamReq(messages: Observable<any>): Promise<any> {
103+
const s = new Subject();
104+
const o = s.asObservable();
105+
messages.subscribe(
106+
msg => {
107+
s.next({
108+
id: 1,
109+
itemTypes: [1],
110+
shipmentType: {
111+
from: 'test',
112+
to: 'test1',
113+
carrier: 'test-carrier',
114+
},
115+
});
116+
},
117+
null,
118+
() => s.complete(),
119+
);
120+
return o;
121+
}
122+
123+
@GrpcStreamCall('orders.OrderService')
124+
async streamReqCall(stream: any, callback: Function) {
125+
stream.on('data', (msg: any) => {
126+
// process msg
127+
});
128+
stream.on('end', () => {
129+
callback(null, {
130+
id: 1,
131+
itemTypes: [1],
132+
shipmentType: {
133+
from: 'test',
134+
to: 'test1',
135+
carrier: 'test-carrier',
136+
},
137+
});
138+
});
139+
}
100140
}

integration/microservices/src/grpc-advanced/proto/orders/service.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ service OrderService {
66
rpc Find(Order) returns (Order);
77
rpc Sync(stream Order) returns (stream Order);
88
rpc SyncCall(stream Order) returns (stream Order);
9+
rpc StreamReq(stream Order) returns (Order);
10+
rpc StreamReqCall(stream Order) returns (Order);
911
}

packages/microservices/server/server-grpc.ts

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,19 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
182182
// streaming from the side of requester
183183
if (protoNativeHandler.requestStream) {
184184
// If any handlers were defined with GrpcStreamMethod annotation use RX
185-
if (streamType === GrpcMethodStreamingType.RX_STREAMING)
186-
return this.createStreamDuplexMethod(methodHandler);
185+
if (streamType === GrpcMethodStreamingType.RX_STREAMING) {
186+
return this.createRequestStreamMethod(
187+
methodHandler,
188+
protoNativeHandler.responseStream,
189+
);
190+
}
187191
// If any handlers were defined with GrpcStreamCall annotation
188-
else if (streamType === GrpcMethodStreamingType.PT_STREAMING)
189-
return this.createStreamCallMethod(methodHandler);
192+
else if (streamType === GrpcMethodStreamingType.PT_STREAMING) {
193+
return this.createStreamCallMethod(
194+
methodHandler,
195+
protoNativeHandler.responseStream,
196+
);
197+
}
190198
}
191199
return protoNativeHandler.responseStream
192200
? this.createStreamServiceMethod(methodHandler)
@@ -220,17 +228,23 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
220228
};
221229
}
222230

223-
public createStreamDuplexMethod(methodHandler: Function) {
224-
return async (call: GrpcCall) => {
231+
public createRequestStreamMethod(
232+
methodHandler: Function,
233+
isResponseStream: boolean,
234+
) {
235+
return async (
236+
call: GrpcCall,
237+
callback: (err: unknown, value: unknown) => void,
238+
) => {
225239
const req = new Subject<any>();
226240
call.on('data', (m: any) => req.next(m));
227241
call.on('error', (e: any) => {
228242
// Check if error means that stream ended on other end
229-
if (
230-
String(e)
231-
.toLowerCase()
232-
.indexOf('cancelled') > -1
233-
) {
243+
const isCancelledError = String(e)
244+
.toLowerCase()
245+
.indexOf('cancelled');
246+
247+
if (isCancelledError) {
234248
call.end();
235249
return;
236250
}
@@ -241,23 +255,49 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
241255

242256
const handler = methodHandler(req.asObservable());
243257
const res = this.transformToObservable(await handler);
244-
await res
245-
.pipe(
246-
takeUntil(fromEvent(call as any, CANCEL_EVENT)),
247-
catchError(err => {
248-
call.emit('error', err);
249-
return EMPTY;
250-
}),
251-
)
252-
.forEach(m => call.write(m));
253-
254-
call.end();
258+
if (isResponseStream) {
259+
await res
260+
.pipe(
261+
takeUntil(fromEvent(call as any, CANCEL_EVENT)),
262+
catchError(err => {
263+
call.emit('error', err);
264+
return EMPTY;
265+
}),
266+
)
267+
.forEach(m => call.write(m));
268+
269+
call.end();
270+
} else {
271+
const response = await res
272+
.pipe(
273+
takeUntil(fromEvent(call as any, CANCEL_EVENT)),
274+
catchError(err => {
275+
callback(err, null);
276+
return EMPTY;
277+
}),
278+
)
279+
.toPromise();
280+
281+
if (typeof response !== 'undefined') {
282+
callback(null, response);
283+
}
284+
}
255285
};
256286
}
257287

258-
public createStreamCallMethod(methodHandler: Function) {
259-
return async (call: GrpcCall) => {
260-
methodHandler(call);
288+
public createStreamCallMethod(
289+
methodHandler: Function,
290+
isResponseStream: boolean,
291+
) {
292+
return async (
293+
call: GrpcCall,
294+
callback: (err: unknown, value: unknown) => void,
295+
) => {
296+
if (isResponseStream) {
297+
methodHandler(call);
298+
} else {
299+
methodHandler(call, callback);
300+
}
261301
};
262302
}
263303

packages/microservices/test/server/server-grpc.spec.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { of } from 'rxjs';
66
import * as sinon from 'sinon';
77
import { InvalidGrpcPackageException } from '../../errors/invalid-grpc-package.exception';
88
import { ServerGrpc } from '../../server/server-grpc';
9-
import { ClientGrpcProxy } from '../../client';
109

1110
class NoopLogger extends Logger {
1211
log(message: any, context?: string): void {}
@@ -336,9 +335,9 @@ describe('ServerGrpc', () => {
336335
});
337336
describe('when request is a stream', () => {
338337
describe('when stream type is RX_STREAMING', () => {
339-
it('should call "createStreamDuplexMethod"', () => {
338+
it('should call "createRequestStreamMethod"', () => {
340339
const cln = sinon.spy();
341-
const spy = sinon.spy(server, 'createStreamDuplexMethod');
340+
const spy = sinon.spy(server, 'createRequestStreamMethod');
342341
server.createServiceMethod(
343342
cln,
344343
{ requestStream: true } as any,
@@ -428,17 +427,17 @@ describe('ServerGrpc', () => {
428427
});
429428
});
430429

431-
describe('createStreamDuplexMethod', () => {
430+
describe('createRequestStreamMethod', () => {
432431
it('should wrap call into Subject', () => {
433432
const handler = sinon.spy();
434-
const fn = server.createStreamDuplexMethod(handler);
433+
const fn = server.createRequestStreamMethod(handler, false);
435434
const call = {
436435
on: (event, callback) => callback(),
437436
off: sinon.spy(),
438437
end: sinon.spy(),
439438
write: sinon.spy(),
440439
};
441-
fn(call as any);
440+
fn(call as any, sinon.spy());
442441

443442
expect(handler.called).to.be.true;
444443
});
@@ -447,9 +446,9 @@ describe('ServerGrpc', () => {
447446
describe('createStreamCallMethod', () => {
448447
it('should pass through to "methodHandler"', () => {
449448
const handler = sinon.spy();
450-
const fn = server.createStreamCallMethod(handler);
449+
const fn = server.createStreamCallMethod(handler, false);
451450
const args = [1, 2, 3];
452-
fn(args as any);
451+
fn(args as any, sinon.spy());
453452

454453
expect(handler.calledWith(args)).to.be.true;
455454
});

0 commit comments

Comments
 (0)