Skip to content

Commit

Permalink
feat: adding gRPC middleware support (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
Farenheith committed Jan 7, 2023
1 parent 02af339 commit a36a92d
Show file tree
Hide file tree
Showing 10 changed files with 314 additions and 63 deletions.
10 changes: 7 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module.exports = {
overrides: [{
files: ['test/*.ts', 'test/**/*.ts'],
rules: {
'no-magic-numbers': 'off',
'@typescript-eslint/no-magic-numbers': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
Expand Down Expand Up @@ -76,10 +76,11 @@ module.exports = {
'no-fallthrough': 'error',
'no-invalid-regexp': 'error',
'no-invalid-this': 'off',
'no-magic-numbers': [
'no-magic-numbers': 'off',
'@typescript-eslint/no-magic-numbers': [
'error',
{
ignore: [0, 1, -1],
ignore: [0, 1, 2, -1],
},
],
'no-multiple-empty-lines': [
Expand All @@ -102,6 +103,9 @@ module.exports = {
'no-var': 'error',
'object-shorthand': 'error',
'one-var': ['error', 'never'],
"@typescript-eslint/no-floating-promises": "error",
"no-return-await": "off",
"@typescript-eslint/return-await": "error",
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports-ts': 'error',
'unused-imports/no-unused-vars-ts': [
Expand Down
4 changes: 3 additions & 1 deletion src/client-config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ChannelOptions } from '@grpc/grpc-js';
import { Options as PackageOptions } from '@grpc/proto-loader';
import { GrpcMiddlewares, GrpcServiceDefinition } from './types';

export interface ClientConfig {
export interface ClientConfig<TService extends GrpcServiceDefinition = any> {
url: string;
protoFile: string;
namespace: string;
Expand All @@ -10,4 +11,5 @@ export interface ClientConfig {
maxConnections: number;
PackageOptions?: PackageOptions;
grpcOptions?: Partial<ChannelOptions>;
middlewares?: GrpcMiddlewares<TService>;
}
13 changes: 9 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GrpcServiceClient, GrpcServiceDefinition } from './types';
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
GrpcObject,
Expand All @@ -11,13 +12,13 @@ import { ClientConfig } from './client-config';
import { ClientPool } from './client-pool';
import { overloadServices } from './utils/overload-services';

export class Client<T> {
export class Client<T extends GrpcServiceDefinition<keyof T>> {
private packageDefinition!: GrpcObject;
private grpcInstance: T;
readonly config: ClientConfig;
readonly config: ClientConfig<T>;
public poolPosition?: number;

constructor(config: ClientConfig, poolService = ClientPool) {
constructor(config: ClientConfig<T>, poolService = ClientPool) {
this.config = config;
if (config.maxConnections === 0) {
this.grpcInstance = this.createClient(config);
Expand Down Expand Up @@ -51,7 +52,11 @@ export class Client<T> {
return current;
}, grpcPackage)[config.service] as ServiceClientConstructor;

const client = new grpcDef(config.url, credentials, config.grpcOptions);
const client = new grpcDef(
config.url,
credentials,
config.grpcOptions,
) as GrpcServiceClient;
const grpcClient = overloadServices(client, config) as unknown as T;
return grpcClient as unknown as T;
}
Expand Down
7 changes: 4 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { Client } from './client';
export { ClientPool } from './client-pool';
export { ClientConfig } from './client-config';
export * from './client';
export * from './client-config';
export * from './client-pool';
export * from './types';
99 changes: 99 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
CallOptions,
Metadata,
ClientDuplexStream,
ClientReadableStream,
ClientWritableStream,
Client,
} from '@grpc/grpc-js';

export interface RawUnaryCall<P, R> {
(param: P, callback: (err: Error, response: R) => void): void;
(
param: P,
deadline: Partial<CallOptions> | undefined,
callback: (err: Error, response: R) => void,
): void;
(
param: P,
metadata: Metadata,
callback: (err: Error, response: R) => void,
): void;
(
param: P,
metadata: Metadata,
deadline: Partial<CallOptions> | undefined,
callback: (err: Error, response: R) => void,
): void;
}

export interface StreamCall<P, R> {
(param: P, deadline?: Partial<CallOptions> | undefined): ClientDuplexStream<
P,
R
>;
(
param: P,
metadata: Metadata,
deadline?: Partial<CallOptions> | undefined,
): ClientDuplexStream<P, R>;
requestStream: ClientWritableStream<P>;
responseStream: ClientReadableStream<R>;
}

export interface UnaryCall<P, R> {
(param: P, deadline?: Partial<CallOptions> | undefined): PromiseLike<R>;
(
param: P,
metadata: Metadata,
deadline?: Partial<CallOptions> | undefined,
): PromiseLike<R>;
}

export type GrpcFunction<P, R> = StreamCall<P, R> | UnaryCall<P, R>;

export function isStreamCall<P, R>(
action: GrpcFunction<P, R>,
): action is StreamCall<P, R> {
return (action as StreamCall<P, R>).requestStream ||
(action as StreamCall<P, R>).responseStream
? true
: false;
}

type KeyType = string | number | symbol;

export type GrpcServiceDefinition<
Methods extends KeyType = keyof GrpcServiceDefinition<KeyType>,
> = Record<Methods, GrpcFunction<any, any>>;
export type GrpcServiceClient<Methods extends KeyType = KeyType> = Client &
GrpcServiceDefinition<Methods>;

export interface FullGrpcParams<
TService extends GrpcServiceDefinition<KeyType>,
k extends keyof TService,
> {
(
param: Parameters<TService[k]>[0],
metadata: Metadata,
options: Partial<CallOptions>,
): void;
}

export interface OtherGrpcParams {
(metadata: Metadata, options: Partial<CallOptions>): void;
}

export type DefaultGrpcMiddleware = (
params: Parameters<OtherGrpcParams>,
) => void;
export type GrpcMethodMiddleware<
TService extends GrpcServiceDefinition<KeyType>,
k extends keyof TService,
> = (params: Parameters<FullGrpcParams<TService, k>>) => void;

export type GrpcMiddlewares<TService extends GrpcServiceDefinition<KeyType>> = {
[k in keyof TService]: GrpcMethodMiddleware<TService, k>[];
} & {
'*'?: DefaultGrpcMiddleware[];
};
84 changes: 84 additions & 0 deletions src/utils/apply-middlewares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
GrpcServiceDefinition,
GrpcMethodMiddleware,
DefaultGrpcMiddleware,
} from './../types';
import { GrpcMiddlewares } from '../types';
import { CallOptions, Metadata } from '@grpc/grpc-js';

function callMiddlewaresFactory<TService extends GrpcServiceDefinition>(
defaultMiddlewares: DefaultGrpcMiddleware[],
methodMiddlewares: GrpcMethodMiddleware<TService, keyof TService>[] = [],
) {
if (!methodMiddlewares.length && !defaultMiddlewares.length) {
return undefined;
}

return (
params: [
Parameters<TService[keyof TService]>[0],
Metadata,
Partial<CallOptions>,
],
) => {
const [, ...others] = params;
for (const middleware of defaultMiddlewares) {
middleware(others);
}
params[1] = others[0];
params[2] = others[1];
for (const middleware of methodMiddlewares) {
middleware(params);
}
};
}

function getNewParameters<TService extends GrpcServiceDefinition>(
args: [
Parameters<TService[keyof TService]>[0],
Metadata | Partial<CallOptions> | undefined,
Partial<CallOptions> | undefined,
],
): [Parameters<TService[keyof TService]>[0], Metadata, Partial<CallOptions>] {
const [params] = args;
let [, metadata, options] = args;
if (!(metadata instanceof Metadata)) {
options = metadata ? metadata : {};
metadata = new Metadata();
} else if (!options) {
options = {};
}
return [params, metadata, options];
}

export function applyMiddlewares<TService extends GrpcServiceDefinition>(
client: TService,
serviceMiddlewares?: GrpcMiddlewares<TService>,
) {
if (serviceMiddlewares) {
const defaultMiddlewares = serviceMiddlewares['*'] ?? [];
for (const k in client) {
if (client.hasOwnProperty(k)) {
const middlewares = serviceMiddlewares[k];
const callMiddlewares = callMiddlewaresFactory(
defaultMiddlewares,
middlewares,
);
if (callMiddlewares) {
const rawCall = client[k]!;
client[k] = ((
...args: [
Parameters<TService[keyof TService]>[0],
Partial<CallOptions> | Metadata | undefined,
Partial<CallOptions> | undefined,
]
) => {
const newArgs = getNewParameters<TService>(args);
callMiddlewares.call(serviceMiddlewares, newArgs);
return (rawCall as Function).apply(client, newArgs);
}) as TService[Extract<keyof TService, string>];
}
}
}
}
}

0 comments on commit a36a92d

Please sign in to comment.