UNPKG

@redpc/core

Version:

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

306 lines (217 loc) 7.82 kB
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);