UNPKG

@redpc/core

Version:

Remoted Procedure Call (RedPC) - A javascript RPC library written with TypeScript

370 lines (267 loc) 9.87 kB
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; }