@redpc/core
Version:
Remoted Procedure Call (RedPC) - A javascript RPC library written with TypeScript
370 lines (267 loc) • 9.87 kB
text/typescript
import {
ContractReference,
HttpMethods,
KnownTransports,
ProxyContractConfig,
ProxyMethodConfig,
StubContractConfig
} 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?: StubContractConfig;
isFrozen: boolean;
isContractConfigured: boolean;
}
interface CallContext {
typeContext: TypeContext;
memberContext: TypeMemberContext;
transport: ITransport;
endpoint: string;
target: any;
fn: Function;
transportConfig: StubTransportConfig;
transportProvider: ITransportProvider;
interceptorProvider: IInterceptorProvider;
}
export interface StubInMemoryTransportConfig {
type: 'in-memory';
}
export interface StubHttpTransportConfig {
type: 'http';
defaultHttpMethod: HttpMethods;
}
export interface StubElectronTransportConfig {
type: 'electron';
}
export interface StubPipeTransportConfig {
type: 'pipe';
}
export interface StubMessageBusTransportConfig {
type: 'message-bus';
}
export interface StubCustomTransportConfig {
type: 'custom';
driver: string;
}
export type StubTransportConfig =
StubInMemoryTransportConfig |
StubHttpTransportConfig |
StubElectronTransportConfig |
StubPipeTransportConfig |
StubMessageBusTransportConfig |
StubCustomTransportConfig
;
export interface StubConfig {
transport: StubTransportConfig;
timeoutInMilliseconds?: number;
}
export interface StubContext extends IEndpointContext, IDisposable {
addStub<T>(
contract: ContractReference<T>,
config?: StubConfig
): IDisposable;
}
export interface StubContextMutator extends StubContext {
defineMember(
config: ProxyMethodConfig,
target: Object | Function,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<any>
): TypedPropertyDescriptor<any>;
defineContract<TFunction extends Function>(
config: StubContractConfig,
constructor: TFunction
): void | TFunction;
}
export class StubContextImpl implements StubContextMutator {
private isHandlingReceivedMessages: boolean = false;
private endpoints: Record<string, CallContext>;
constructor(
private contextDomain: string,
private transportProvider: ITransportProvider,
private interceptorProvider: IInterceptorProvider
) {
this.endpoints = {};
this.onMessageReceived = this.onMessageReceived.bind(this);
}
dispose(): Promise<void> {
throw new Error("Method not implemented.");
}
addStub<T>(
contract: ContractReference<T>,
config?: StubConfig
): IDisposable {
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;
const endpoints: Record<string, CallContext> = {};
for (const member of metaData.members) {
const memberName = member.config.name ?? member.propertyKey.toString();
const endpoint = `${path}/${memberName}`;
if (this.endpoints[endpoint]) {
throw new RpcError(`Duplicate endpoint '${endpoint}' found`);
}
const callContext: CallContext = {
memberContext: member,
typeContext: metaData,
transport,
endpoint,
target,
fn: undefined!,
transportConfig: config.transport,
interceptorProvider: this.interceptorProvider,
transportProvider: this.transportProvider
};
callContext.fn = Stub_CallAsyncMethod.bind(callContext);
endpoints[endpoint] = callContext;
}
for (const key in endpoints) {
if (Object.prototype.hasOwnProperty.call(endpoints, key)) {
const element = endpoints[key];
this.endpoints[key] = element;
}
}
metaData.isFrozen = true;
if (!this.isHandlingReceivedMessages) {
transport.subscribe(this.onMessageReceived);
this.isHandlingReceivedMessages = true;
}
return {
dispose: async () => {
}
};
}
private async onMessageReceived(msg: IMessage): Promise<IMessage> {
switch (msg.type) {
case 'call':
const callMessage = msg as MethodCallMessage;
const endpointCtx = this.findEndpoint(callMessage.endpoint);
if (!endpointCtx) {
return {
type: 'error',
errorCode: 404,
errorMessage: 'Endpoint was not found'
} as ErrorMessage;
} else {
let result: any;
let isError = false;
let errorMessage;
try {
result = await endpointCtx.fn(...callMessage.args);
} catch (e: any) {
isError = true;
errorMessage = e?.toString();
}
const responseMessage: IMessage = isError
? {
type: "error",
errorCode: 500,
errorMessage
} as ErrorMessage
: {
type: "result",
value: result
} as MethodCallResultMessage;
return responseMessage;
}
default: return {
type: 'error',
errorCode: 400,
errorMessage: 'Unknown message type'
} as ErrorMessage
}
}
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: StubContractConfig,
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;
}
private findEndpoint(path: string): CallContext {
const endpoint = this.endpoints[path] as CallContext;
return endpoint;
}
}
export const StubContext = new ContextDomain<StubContext>(StubContextImpl);
async function Stub_CallAsyncMethod(this: CallContext, ...args: any[]): Promise<any> {
const targetFunction = this.target[this.memberContext.propertyKey].bind(this.target);
const result = await targetFunction(...args);
return result;
}