@redpc/core
Version:
Remoted Procedure Call (RedPC) - A javascript RPC library written with TypeScript
306 lines (217 loc) • 7.82 kB
text/typescript
import {
ContractReference,
HttpMethods,
KnownTransports,
ProxyContractConfig,
ProxyMethodConfig
} from "../types";
import { ContextDomain } from "./ContextDomain";
import { IEndpointContext } from "./IEndpointContext";
import { RpcError } from "errors";
import { ITransportProvider } from "./ITransportProvider";
import { IInterceptorProvider } from "./IInterceptorProvider";
import { ErrorMessage, IMessage, MethodCallMessage, MethodCallResultMessage } from "./IMessage";
import "reflect-metadata";
import { ITransport } from "./ITransport";
import { IDisposable } from "./IDisposable";
interface TypeMemberContext {
propertyKey: string | symbol;
descriptor: TypedPropertyDescriptor<any>;
config: ProxyMethodConfig;
}
interface TypeContext {
constructor: Function;
members: TypeMemberContext[];
config?: ProxyContractConfig;
isFrozen: boolean;
isContractConfigured: boolean;
}
interface CallContext {
typeContext: TypeContext;
memberContext: TypeMemberContext;
transport: ITransport;
endpoint: string;
transportConfig: ProxyTransportConfig;
transportProvider: ITransportProvider;
interceptorProvider: IInterceptorProvider;
}
export interface ProxyInMemoryTransportConfig {
type: 'in-memory';
}
export interface ProxyHttpTransportConfig {
type: 'http';
defaultHttpMethod: HttpMethods;
}
export interface ProxyElectronTransportConfig {
type: 'electron';
}
export interface ProxyPipeTransportConfig {
type: 'pipe';
}
export interface ProxyMessageBusTransportConfig {
type: 'message-bus';
}
export interface ProxyCustomTransportConfig {
type: 'custom';
driver: string;
}
export type ProxyTransportConfig =
ProxyInMemoryTransportConfig |
ProxyHttpTransportConfig |
ProxyElectronTransportConfig |
ProxyPipeTransportConfig |
ProxyMessageBusTransportConfig |
ProxyCustomTransportConfig
;
export interface ProxyChannelConfig {
transport: ProxyTransportConfig;
timeoutInMilliseconds?: number;
}
export interface ProxyContext extends IEndpointContext, IDisposable {
createChannel<T>(
contract: ContractReference<T>,
config: ProxyChannelConfig
): T;
}
export interface ProxyContextMutator extends ProxyContext {
defineMember(
config: ProxyMethodConfig,
target: Object | Function,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<any>
): TypedPropertyDescriptor<any>;
defineContract<TFunction extends Function>(
config: ProxyContractConfig,
constructor: TFunction
): void | TFunction
}
class ProxyContextImpl implements ProxyContextMutator {
constructor(
private contextDomain: string,
private transportProvider: ITransportProvider,
private interceptorProvider: IInterceptorProvider
) {
}
dispose(): Promise<void> {
return Promise.resolve();
}
createChannel<T>(
contract: ContractReference<T>,
config: ProxyChannelConfig
): T {
if (!config) {
throw new RpcError('Configuration is required.');
}
if (typeof config.transport === 'undefined') {
throw new RpcError('A transport must be configured.');
}
const name = typeof contract === 'string'
? contract
: contract.name;
const metaData = Reflect.getOwnMetadata(name, this) as TypeContext;
if (!metaData.isContractConfigured) {
throw new RpcError('Contract was not found.');
}
const transport = this.transportProvider.getTransport(config.transport.type);
const contractConfig = metaData.config!;
const target = (new (metaData.constructor as any)) as T;
const path = contractConfig.path;
for (const member of metaData.members) {
const memberName = member.config.name ?? member.propertyKey.toString();
const endpoint = `${path}/${memberName}`;
const callContext: CallContext = {
memberContext: member,
typeContext: metaData,
transport,
endpoint,
transportConfig: config.transport,
interceptorProvider: this.interceptorProvider,
transportProvider: this.transportProvider
};
(target as Record<string | symbol, any>)[member.propertyKey] = Proxy_CallAsyncMethod.bind(callContext);
}
metaData.isFrozen = true;
return target;
}
public defineMember(
config: ProxyMethodConfig,
target: Object | Function,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<any>
): TypedPropertyDescriptor<any> {
const ctx = this.getOwnTypeContext(target);
if (ctx.isFrozen) {
throw new RpcError('The new prototype has already been applied to the original prototype.');
}
ctx.members.push({
propertyKey,
descriptor,
config
});
return descriptor;
}
public defineContract<TFunction extends Function>(
config: ProxyContractConfig,
constructor: TFunction
): void | TFunction {
const name = constructor.name;
let ctx = Reflect.getOwnMetadata(name, this) as TypeContext;
if (!!ctx) {
ctx.config = config;
ctx.isContractConfigured = true;
ctx.constructor = constructor;
} else {
Reflect.defineMetadata(name, {
isFrozen: false,
isContractConfigured: true,
config,
constructor,
members: []
} as TypeContext, this);
}
return constructor;
}
private getOwnTypeContext(
target: Object | Function
): TypeContext {
const prototype = target.constructor ?? Object.getPrototypeOf(target);
const name = prototype.name;
let ctx = Reflect.getOwnMetadata(name, this) as TypeContext;
if (typeof ctx === 'undefined') {
ctx = {
isFrozen: false,
isContractConfigured: false,
members: []
};
Reflect.defineMetadata(name, ctx, this);
}
return ctx;
}
}
async function Proxy_TransformResponse(ctx: CallContext, res: IMessage): Promise<any> {
switch (res.type) {
case 'error':
const errorMessage = res as ErrorMessage;
throw new RpcError(errorMessage.errorMessage);
case 'result':
const resultMessage = res as MethodCallResultMessage;
return resultMessage.value;
default:
throw new RpcError('Unknown response found');
}
}
async function Proxy_TransformRequest(ctx: CallContext, req: IMessage): Promise<IMessage> {
// this.interceptorProvider.getInterceptor('').
return req;
}
async function Proxy_CallAsyncMethod(this: CallContext, ...args: any[]): Promise<any> {
const msg: MethodCallMessage = {
type: 'call',
endpoint: this.endpoint,
args
};
const request = await Proxy_TransformRequest(this, msg);
const response = await this.transport.sendAsync(request);
return await Proxy_TransformResponse(this, response);
}
export const ProxyContext = new ContextDomain<ProxyContext>(ProxyContextImpl);