Skip to content

Commit 4b4c179

Browse files
authored
Merge pull request #4 from grissius/feat/type-safety
Type safety
2 parents fb4987e + d638219 commit 4b4c179

File tree

16 files changed

+377
-318
lines changed

16 files changed

+377
-318
lines changed

README.md

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ Modern, minimalist type-safe gRPC framework for Node.js
1111
## Quickstart
1212

1313
```typescript
14-
import { Server } from 'protocat'
14+
import { ProtoCat } from 'protocat'
1515
import { CatService } from '../dist/cat_grpc_pb' // Generated service definition
1616
17-
server = new Server()
18-
server.addService(CatService, {
17+
app = new ProtoCat()
18+
app.addService(CatService, {
1919
getCat: async call => {
2020
const cat = await getCatByName(call.request?.getName() ?? '')
2121
call.response.setName(cat.name)
@@ -25,7 +25,7 @@ server.addService(CatService, {
2525
}
2626
}
2727

28-
server.start('0.0.0.0:3000', ServerCredentials.createInsecure())
28+
app.start('0.0.0.0:3000', ServerCredentials.createInsecure())
2929
```
3030
3131
## Features
@@ -40,19 +40,19 @@ Protocat uses pure JavaScript gRPC client implementation `@grpc/grpc-js`
4040
4141
Middlewares can be registered
4242
43-
1. either globally, with `server.use` for all incoming requests,
43+
1. either globally, with `app.use` for all incoming requests,
4444
2. or at method level with `addService`, where instead of a single handler, an array of handlers can be provided (handler and middleware have the same API).
4545
4646
```typescript
47-
server.use(ctx => {
47+
app.use(call => {
4848
/*...*/
4949
})
50-
server.addService(CatService, {
50+
app.addService(CatService, {
5151
getCat: [
52-
ctx => {
52+
call => {
5353
/*...*/
5454
},
55-
ctx => {
55+
call => {
5656
/*...*/
5757
},
5858
],
@@ -63,21 +63,21 @@ Note that grpc does not provide API to intercept all incoming requests, only to
6363
6464
#### `next` function
6565
66-
Here is an example of a simple logger middleware. Apart from `context` each middleware (handler alike) has a `next` function. This is callstack of all subsequent middlewares and handlers. This feature is demonstrated in a simple logger middleware bellow.
66+
Here is an example of a simple logger middleware. Apart from `call` each middleware (handler alike) has a `next` function. This is callstack of all subsequent middlewares and handlers. This feature is demonstrated in a simple logger middleware bellow.
6767
6868
```typescript
69-
server.use(async (ctx, next) => {
69+
app.use(async (call, next) => {
7070
const start = performance.now()
71-
console.log(` --> ${ctx.path}`, {
72-
request: ctx.request?.toObject(),
73-
clientMetadata: ctx.metadata.getMap(),
71+
console.log(` --> ${call.path}`, {
72+
request: call.request?.toObject(),
73+
clientMetadata: call.metadata.getMap(),
7474
})
7575
await next()
76-
console.log(` <-- ${ctx.path}`, {
77-
response: ctx.response?.toObject(),
76+
console.log(` <-- ${call.path}`, {
77+
response: call.response?.toObject(),
7878
durationMillis: performance.now() - start,
79-
initialMetadata: ctx.initialMetadata.getMap(),
80-
trailingMetadata: ctx.trailingMetadata.getMap(),
79+
initialMetadata: call.initialMetadata.getMap(),
80+
trailingMetadata: call.trailingMetadata.getMap(),
8181
})
8282
})
8383
```
@@ -87,17 +87,17 @@ server.use(async (ctx, next) => {
8787
All middlewares are executed in order they were registered, followed by an execution of handlers is provided order, regardless of middleware-service order. Not that in the following example, `C` middleware is registered after `CatService` and it is still called, even before the handlers.
8888
8989
```typescript
90-
server.use(async (ctx, next) => {
90+
app.use(async (call, next) => {
9191
console.log('A1')
9292
await next()
9393
console.log('A2')
9494
})
95-
server.use(async (ctx, next) => {
95+
app.use(async (call, next) => {
9696
console.log('B1')
9797
await next()
9898
console.log('B2')
9999
})
100-
server.addService(CatService, {
100+
app.addService(CatService, {
101101
getCat: [
102102
async (call, next) => {
103103
console.log('D1')
@@ -111,7 +111,7 @@ server.addService(CatService, {
111111
},
112112
],
113113
})
114-
server.use(async (ctx, next) => {
114+
app.use(async (call, next) => {
115115
console.log('C1')
116116
await next()
117117
console.log('C2')
@@ -137,7 +137,7 @@ server.use(async (ctx, next) => {
137137
Error handling can be solved with a custom simple middleware, thanks to existing `next` cascading mechanism:
138138
139139
```typescript
140-
server.use(async (ctx, next) => {
140+
app.use(async (call, next) => {
141141
try {
142142
await next()
143143
} catch (error) {
@@ -158,17 +158,17 @@ There is an `onError` middleware creator, that can intercept all errors includin
158158
```typescript
159159
import { onError } from 'protocat'
160160

161-
server.use(
162-
onError((e, ctx) => {
161+
app.use(
162+
onError((e, call) => {
163163
// Set metadata
164-
ctx.initialMetadata.set('error-code', e.code)
165-
ctx.trailingMetadata.set('error-code', e.code)
164+
call.initialMetadata.set('error-code', e.code)
165+
call.trailingMetadata.set('error-code', e.code)
166166

167167
// Consume the error
168168
if (notThatBad(e)) {
169-
if (ctx.type === CallType.SERVER_STREAM || ctx.type === CallType.BIDI) {
169+
if (call.type === CallType.ServerStream || call.type === CallType.Bidi) {
170170
// sync error not re-thrown on stream response, should end
171-
ctx.end()
171+
call.end()
172172
}
173173
return
174174
}
@@ -182,7 +182,7 @@ server.use(
182182
)
183183
```
184184
185-
- The handler is called with error and context for all errors (rejects from handlers, error emits from streams), meaning there can be theoretically more errors per request (multiple emitted errors) and some of them can be handled even after the executon of the next chain (error emits).
185+
- The handler is called with error and current call for all errors (rejects from handlers, error emits from streams), meaning there can be theoretically more errors per request (multiple emitted errors) and some of them can be handled even after the executon of the next chain (error emits).
186186
- Provided function can be sync on async. It can throw (or return rejected promise), but any other return value is ignored
187187
- Both initial and trailing metadata are available for change (unless you sent them manually)
188188
- In order to achieve "re-throwing", `emit` function on call is patched by `onError`. When calling `call.emit('error', e)`, the error is actually emitted in the stream only when the handler throws a new error. This means that when you emit an error in the middleware and consume it in the handler, streams are left "hanging", not errored and likely not even ended. If you truly wish to not propagate the error to client, it is recommended to end the streams in the handler. (This is not performed automatically, since there is no guarantee there should be no more than one error)
@@ -193,16 +193,16 @@ server.use(
193193
194194
- [x] Middleware
195195
- [x] Error handling
196-
- [ ] Type safety
196+
- [x] Type safety
197197
198-
- [ ] call / context terminology
199-
- [ ] call / stream terminology
200-
- [ ] metaS / metaP test naming confusion
198+
- [x] call / context terminology
199+
- [x] call / stream terminology
200+
- [x] status / metadata test naming confusion
201201
- [ ] metadata readme section
202202
- [ ] starter project
203203
- [ ] gRPC client
204204
- [ ] Call pool
205-
- [ ] Context type extension
205+
- [x] Context type extension
206206
- [ ] Partial definition
207207
- [ ] Serialization level caching
208208
- [ ] Docs

src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { Server } from './lib/server'
1+
export { ProtoCat } from './lib/server/application'
22
export { CallType } from './lib/call-types'
3-
export { onError } from './lib/middleware/on-error'
4-
export { Middleware } from './lib/context'
3+
export { onError } from './lib/server/middleware/on-error'
4+
export { Middleware, ServiceImplementation } from './lib/server/call'

src/lib/call-types.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/** Call types aligned with grpc core library */
22
export enum CallType {
3-
BIDI = 'bidi',
4-
SERVER_STREAM = 'serverStream',
5-
CLIENT_STREAM = 'clientStream',
6-
UNARY = 'unary',
3+
Bidi = 'bidi',
4+
ServerStream = 'serverStream',
5+
ClientStream = 'clientStream',
6+
Unary = 'unary',
77
}
88

99
/** Assign call type from generated definition */
@@ -12,8 +12,8 @@ export const stubToType = (
1212
) =>
1313
s.responseStream
1414
? s.requestStream
15-
? CallType.BIDI
16-
: CallType.SERVER_STREAM
15+
? CallType.Bidi
16+
: CallType.ServerStream
1717
: s.requestStream
18-
? CallType.CLIENT_STREAM
19-
: CallType.UNARY
18+
? CallType.ClientStream
19+
: CallType.Unary

src/lib/context.ts

Lines changed: 0 additions & 84 deletions
This file was deleted.

src/lib/misc/type-helpers.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { EventEmitter } from 'events'
2+
3+
// List object keys without index signatures
4+
// KnownKeys<{ [index: string]: string; foo: string }> = 'foo'
5+
type KnownKeys<T> = {
6+
[K in keyof T]: string extends K ? never : number extends K ? never : K
7+
} extends { [_ in keyof T]: infer U }
8+
? U
9+
: never
10+
11+
// Remove index signature keys from object
12+
// RemoveIdxSgn<{ [index: string]: string; foo: string }> = { foo: string }
13+
export type RemoveIdxSgn<T> = Pick<T, KnownKeys<T>>
14+
15+
export type TypedOnData<E extends EventEmitter, T> = Omit<E, 'on'> & {
16+
on(event: 'close', listener: () => void): E
17+
on(event: 'data', listener: (chunk: T) => void): E
18+
on(event: 'end', listener: () => void): E
19+
on(event: 'error', listener: (err: Error) => void): E
20+
on(event: 'pause', listener: () => void): E
21+
on(event: 'readable', listener: () => void): E
22+
on(event: 'resume', listener: () => void): E
23+
on(event: string | symbol, listener: (...args: any[]) => void): E
24+
}
Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
11
import * as grpc from '@grpc/grpc-js'
22
import { ChannelOptions } from '@grpc/grpc-js/build/src/channel-options'
3-
import { ServiceImplementation, Middleware } from './context'
4-
import { stubToType } from './call-types'
5-
import { bindAsync, tryShutdown } from './grpc-helpers'
6-
import { composeMiddleware } from './middleware'
3+
import { ServiceImplementation, Middleware } from './call'
4+
import { stubToType } from '../call-types'
5+
import { bindAsync, tryShutdown } from '../misc/grpc-helpers'
6+
import { composeMiddleware } from './middleware/compose-middleware'
77

8-
export class Server {
9-
/** Underlaying gRPC server */
8+
export class ProtoCat<Extension = {}> {
9+
/** Underlying gRPC server */
1010
public server: grpc.Server
1111
/** Map of loaded generated method stubs */
1212
public methodDefinitions: Record<string, grpc.MethodDefinition<any, any>>
1313
/** Map of loaded method service implementations */
1414
public serviceHandlers: Record<string, Middleware[]>
1515
/** Global middleware functions */
16-
public middleware: Middleware[]
16+
public middleware: Array<Middleware<Extension>>
1717
constructor(options?: ChannelOptions) {
1818
this.server = new grpc.Server(options)
1919
this.methodDefinitions = {}
2020
this.middleware = []
2121
this.serviceHandlers = {}
2222
}
2323

24-
public use(...middleware: Middleware[]) {
24+
public use(...middleware: Array<Middleware<Extension>>) {
2525
this.middleware.push(...middleware)
2626
}
2727

2828
public addService<
2929
T extends grpc.ServiceDefinition<grpc.UntypedServiceImplementation>
30-
>(serviceDefinition: T, serviceImplementation: ServiceImplementation<T>) {
30+
>(
31+
serviceDefinition: T,
32+
serviceImplementation: ServiceImplementation<T, Extension>
33+
) {
3134
for (const methodName in serviceDefinition) {
3235
// Path is FQN with namespace to avoid collisions
3336
const key = serviceDefinition[methodName].path
@@ -87,23 +90,23 @@ const wrapToHandler = (
8790
}
8891

8992
return async (
90-
call: any, // grpc.ServerReadableStream<any, any> | grpc.ServerUnaryCall<any, any> | grpc.ServerDuplexStream<any, any> | grpc.ServerWritableStream<any, any>
93+
grpcCall: any, // grpc.ServerReadableStream<any, any> | grpc.ServerUnaryCall<any, any> | grpc.ServerDuplexStream<any, any> | grpc.ServerWritableStream<any, any>
9194
cb?: grpc.sendUnaryData<any> // Only for call grpc.ServerReadableStream<any, any> | grpc.ServerUnaryCall<any, any>, missing otherwise
9295
) => {
93-
const ctx = createContext(call)
96+
const call = createContext(grpcCall)
9497
// @ts-ignore: Not part of public API, but only way to pair message to method
95-
ctx.response = new methodDefinition.responseType()
98+
call.response = new methodDefinition.responseType()
9699
try {
97-
await methodHandler(ctx)
98-
ctx.sendMetadata(ctx.initialMetadata)
100+
await methodHandler(call)
101+
call.sendMetadata(call.initialMetadata)
99102
if (cb) {
100-
cb(null, ctx.response, ctx.trailingMetadata)
103+
cb(null, call.response, call.trailingMetadata)
101104
}
102105
} catch (e) {
103106
if (cb) {
104-
cb(e, null, ctx.trailingMetadata)
107+
cb(e, null, call.trailingMetadata)
105108
} else {
106-
call.emit('error', e)
109+
grpcCall.emit('error', e)
107110
}
108111
}
109112
}

0 commit comments

Comments
 (0)