Skip to content

Commit c2090c6

Browse files
author
Milan Pavlik
committed
Implement grpc-web middleware
1 parent fafc32e commit c2090c6

15 files changed

+641
-136
lines changed

app/src/App.tsx

-32
Original file line numberDiff line numberDiff line change
@@ -217,43 +217,11 @@ const ThemingLayout = () => (
217217

218218
class App extends React.Component {
219219

220-
componentDidMount() {
221-
let count = 0;
222-
const request = new ListStoriesRequest();
223-
grpc.invoke(HackerNewsService.ListStories, {
224-
request: request,
225-
debug: true,
226-
host: 'http://localhost:8900',
227-
onMessage: (res: ListStoriesResponse) => {
228-
count += 1;
229-
// const obj: ListStoriesResponse = res.toObject();
230-
const url = res.getStory()!.getUrl();
231-
console.log('url', url);
232-
// console.log(id);
233-
if (count === 1) {
234-
fetch('http://localhost:8900/article-proxy?q=' + encodeURIComponent(url));
235-
}
236-
// grpc.unary(HackerNewsService.GetStory, {
237-
// host: 'http://localhost:8900',
238-
// debug: true,
239-
// request: req,
240-
// onEnd: (result) => {
241-
// console.log(result.message ? atob((result.message.toObject() as {html: string}).html) : 'nil');
242-
// }
243-
// });
244-
},
245-
onEnd: (res) => {
246-
console.log('end', res);
247-
}
248-
});
249-
}
250-
251220
render() {
252221
return (
253222
<Container style={{marginTop: '3em'}}>
254223
<ThemingLayout/>
255224
</Container>
256-
257225
);
258226
}
259227
}

app/src/Stories.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Container, Grid, Header } from 'semantic-ui-react';
66
import StoryList from './StoryList';
77
import StoryView from './StoryView';
88
import { RootAction } from './actions';
9-
import { listStoriesInit } from './actions/stories';
9+
import { listStories } from './actions/stories';
1010

1111
type StoriesProps = {
1212
stories: Story[],
@@ -18,7 +18,7 @@ type StoriesProps = {
1818
class Stories extends React.Component<StoriesProps, {}> {
1919

2020
componentDidMount() {
21-
this.props.dispatch(listStoriesInit());
21+
this.props.dispatch(listStories());
2222
}
2323

2424
render() {

app/src/actions/stories.ts

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,51 @@
1+
import { Action } from 'redux';
2+
import { ListStoriesRequest, ListStoriesResponse } from '../proto/hackernews_pb';
3+
import { GrpcAction, grpcRequest } from '../middleware/grpc';
4+
import { Code, Metadata } from 'grpc-web-client';
5+
import { HackerNewsService } from '../proto/hackernews_pb_service';
6+
import { PingRequest, PingResponse } from '../proto/ping_pb';
7+
import { PingService } from '../proto/ping_pb_service';
8+
19
export const STORIES_INIT = 'STORIES_INIT';
210

311
type ListStoriesInit = {
412
type: typeof STORIES_INIT,
513
};
614

15+
export const ping = () => {
16+
return grpcRequest<PingRequest, PingResponse>({
17+
request: new PingRequest(),
18+
onEnd: (code: Code, message: string | undefined, trailers: Metadata): Action | void => {
19+
return;
20+
},
21+
host: 'http://localhost:8900',
22+
methodDescriptor: PingService.Ping,
23+
onMessage: (message) => {
24+
console.log(message);
25+
return;
26+
}
27+
});
28+
};
29+
30+
export const listStories = () => {
31+
return grpcRequest<ListStoriesRequest, ListStoriesResponse>({
32+
request: new ListStoriesRequest(),
33+
onStart: () => listStoriesInit(),
34+
onEnd: (code: Code, message: string | undefined, trailers: Metadata): Action | void => {
35+
console.log(code, message, trailers);
36+
return;
37+
},
38+
host: 'http://localhost:8900',
39+
methodDescriptor: HackerNewsService.ListStories,
40+
onMessage: message => {
41+
console.log(message);
42+
return;
43+
},
44+
});
45+
};
46+
747
export const listStoriesInit = (): ListStoriesInit => ({type: STORIES_INIT});
848

949
export type StoryActionTypes =
10-
| ListStoriesInit;
50+
| ListStoriesInit
51+
| GrpcAction<ListStoriesRequest, ListStoriesResponse>;

app/src/middleware/grpc.ts

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { Action, Dispatch, Middleware, MiddlewareAPI } from 'redux';
2+
import { Code, grpc, Metadata, Transport } from 'grpc-web-client';
3+
import * as jspb from 'google-protobuf';
4+
5+
const GRPC_WEB_REQUEST = 'GRPC_WEB_REQUEST';
6+
// const GRPC_WEB_INVOKE = 'GRPC_WEB_INVOKE';
7+
8+
// Descriptor of a grpc-web payload
9+
// life-cycle methods mirror grpc-web but allow for an action to be dispatched when triggered
10+
export type GrpcActionPayload<RequestType extends jspb.Message, ResponseType extends jspb.Message> = {
11+
// The method descriptor to use for a gRPC request, equivalent to grpc.invoke(methodDescriptor, ...)
12+
methodDescriptor: grpc.MethodDefinition<RequestType, ResponseType>,
13+
// The transport to use for grpc-web, automatically selected if empty
14+
transport?: Transport,
15+
// toggle debug messages
16+
debug?: boolean,
17+
// the URL of a host this request should go to
18+
host: string,
19+
// An instance of of the request message
20+
request: RequestType,
21+
// Additional metadata to attach to the request, the same as grpc-web
22+
metadata?: Metadata.ConstructorArg,
23+
// Called immediately before the request is started, useful for toggling a loading status
24+
onStart?: () => Action | void,
25+
// Called when response headers are received
26+
onHeaders?: (headers: Metadata) => Action | void,
27+
// Called on each incoming message
28+
onMessage?: (res: ResponseType) => Action | void,
29+
// Called at the end of a request, make sure to check the exit code
30+
onEnd: (code: Code, message: string, trailers: Metadata) => Action | void,
31+
};
32+
33+
// Basic type for a gRPC Action
34+
export type GrpcAction<RequestType extends jspb.Message, ResponseType extends jspb.Message> = {
35+
type: typeof GRPC_WEB_REQUEST,
36+
payload: GrpcActionPayload<RequestType, ResponseType>,
37+
};
38+
39+
// Action creator, Use it to create a new grpc action
40+
export function grpcRequest<RequestType extends jspb.Message, ResponseType extends jspb.Message>(
41+
payload: GrpcActionPayload<RequestType, ResponseType>
42+
): GrpcAction<RequestType, ResponseType> {
43+
return {
44+
type: GRPC_WEB_REQUEST,
45+
payload,
46+
};
47+
}
48+
49+
/* tslint:disable:no-any*/
50+
export function newGrpcMiddleware(): Middleware {
51+
return ({getState, dispatch}: MiddlewareAPI<{}>) => (next: Dispatch<{}>) => (action: any) => {
52+
// skip non-grpc actions
53+
if (!isGrpcWebUnaryAction(action)) {
54+
return next(action);
55+
}
56+
57+
const payload = action.payload;
58+
59+
if (payload.onStart) {
60+
payload.onStart();
61+
}
62+
63+
grpc.invoke(payload.methodDescriptor, {
64+
debug: payload.debug,
65+
host: payload.host,
66+
request: payload.request,
67+
metadata: payload.metadata,
68+
transport: payload.transport,
69+
onHeaders: headers => {
70+
if (!payload.onHeaders) { return; }
71+
const actionToDispatch = payload.onHeaders(headers);
72+
return actionToDispatch && dispatch(actionToDispatch);
73+
},
74+
onMessage: res => {
75+
if (!payload.onMessage) { return; }
76+
const actionToDispatch = payload.onMessage(res);
77+
return actionToDispatch && dispatch(actionToDispatch);
78+
},
79+
onEnd: (code, msg, trailers) => {
80+
const actionToDispatch = payload.onEnd(code, msg, trailers);
81+
return actionToDispatch && dispatch(actionToDispatch);
82+
},
83+
});
84+
85+
return next(action);
86+
};
87+
}
88+
89+
function isGrpcWebUnaryAction(action: any): action is GrpcAction<jspb.Message, jspb.Message> {
90+
return action && action.type && action.type === GRPC_WEB_REQUEST && isGrpcWebPayload(action);
91+
}
92+
93+
function isGrpcWebPayload(action: any): boolean {
94+
return action &&
95+
action.payload &&
96+
action.payload.methodDescriptor &&
97+
action.payload.request &&
98+
action.payload.onEnd &&
99+
action.payload.host;
100+
}
101+
102+
/* tslint:enable:no-any*/

app/src/proto/hackernews_pb_service.ts

-8
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,4 @@ export namespace HackerNewsService {
1414
static readonly requestType = proto_hackernews_pb.ListStoriesRequest;
1515
static readonly responseType = proto_hackernews_pb.ListStoriesResponse;
1616
}
17-
export class GetStory {
18-
static readonly methodName = "GetStory";
19-
static readonly service = HackerNewsService;
20-
static readonly requestStream = false;
21-
static readonly responseStream = false;
22-
static readonly requestType = proto_hackernews_pb.GetStoryRequest;
23-
static readonly responseType = proto_hackernews_pb.GetStoryResponse;
24-
}
2517
}

app/src/proto/ping_pb.d.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// package: grpc_web_hacker_news
2+
// file: proto/ping.proto
3+
4+
import * as jspb from "google-protobuf";
5+
6+
export class PingRequest extends jspb.Message {
7+
serializeBinary(): Uint8Array;
8+
toObject(includeInstance?: boolean): PingRequest.AsObject;
9+
static toObject(includeInstance: boolean, msg: PingRequest): PingRequest.AsObject;
10+
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
11+
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
12+
static serializeBinaryToWriter(message: PingRequest, writer: jspb.BinaryWriter): void;
13+
static deserializeBinary(bytes: Uint8Array): PingRequest;
14+
static deserializeBinaryFromReader(message: PingRequest, reader: jspb.BinaryReader): PingRequest;
15+
}
16+
17+
export namespace PingRequest {
18+
export type AsObject = {
19+
}
20+
}
21+
22+
export class PingResponse extends jspb.Message {
23+
serializeBinary(): Uint8Array;
24+
toObject(includeInstance?: boolean): PingResponse.AsObject;
25+
static toObject(includeInstance: boolean, msg: PingResponse): PingResponse.AsObject;
26+
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
27+
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
28+
static serializeBinaryToWriter(message: PingResponse, writer: jspb.BinaryWriter): void;
29+
static deserializeBinary(bytes: Uint8Array): PingResponse;
30+
static deserializeBinaryFromReader(message: PingResponse, reader: jspb.BinaryReader): PingResponse;
31+
}
32+
33+
export namespace PingResponse {
34+
export type AsObject = {
35+
}
36+
}
37+

0 commit comments

Comments
 (0)