import {Application, Component, Configurable, Inject, timeout, Validator} from '@lakutata/core' import {Logger} from '@lakutata/core/build/plugins/Logger' import {ClientComponent} from './ClientComponent' import {TInvokeObject} from '../../types/TInvokeObject' import {TPattern} from '../../types/TPattern' import {TInvokePattern} from '../../types/TInvokePattern' import {ServiceInvokeTimeoutException} from '../../exceptions/ServiceInvokeTimeoutException' import {BrokenServiceInvokeException} from '../../exceptions/BrokenServiceInvokeException' import {format} from 'util' import { SERVICE_DIRECT_INVOKE_PREPARE_EVENT, SERVICE_HTTP_ACCESSIBLE_EVENT, SERVICE_INVOKE_EVENT, SERVICE_INVOKE_ID, SERVICE_PROXY_INVOKE_EVENT } from '../../constants/SocketConstant' import HyperID, {Instance} from 'hyperid' import {IServiceInfoObject} from '../../interfaces/IServiceInfoObject' import cloneDeep from 'clone-deep' export class ConsumerComponent extends Component { @Inject(Application) protected readonly app: Application @Inject('client') protected client: ClientComponent @Configurable() protected readonly timeout: () => number @Configurable() protected readonly retry: () => number @Configurable() protected readonly logger: () => Logger | undefined protected readonly hyperIdInstance: Instance = HyperID({urlSafe: true, fixedLength: true}) protected readonly invokeObjectSchema = Validator.Object(Validator.Any) /** * 初始化函数 * @protected */ protected async initialize(): Promise { //nothing to do } /** * 处理调用 * @param handleObject * @param invokeParam * @protected */ protected async invokeHandler(handleObject: { invokeId: string; chain: string[]; object: TPattern | undefined; beginAt: number; totalTimeSpent: number }, invokeParam: TInvokeObject) { const invokeTimeout: number = invokeParam.timeout ? invokeParam.timeout : this.timeout() const invokePacket: TInvokePattern = { id: invokeParam.id ? invokeParam.id : handleObject.invokeId, [SERVICE_INVOKE_ID]: invokeParam.serviceId, object: (() => { if (!handleObject.object && !invokeParam.data) { throw this.generateException(BrokenServiceInvokeException, 'Empty input data') } if (handleObject.object && invokeParam.data) { try { this.invokeObjectSchema.validateSync(handleObject.object) } catch (e) { throw this.generateException(BrokenServiceInvokeException, `Invalid return value for next invoking: ${format(handleObject.object)}`) } try { this.invokeObjectSchema.validateSync(invokeParam.data) } catch (e) { throw this.generateException(BrokenServiceInvokeException, `Invalid input value for invoking: ${format(invokeParam.data)}`) } return Object.assign(handleObject.object, invokeParam.data) } else { return handleObject.object || invokeParam.data } })(), error: null, chain: handleObject.chain, timeout: invokeTimeout, retry: invokeParam.retry as number, beginAt: handleObject.beginAt, invokeAt: Date.now(), totalTimeSpent: handleObject.totalTimeSpent } //先向服务中心拉取需要访问服务的地址,如果可直接联通,则进行http访问,不经过socket.io const invokePacketForPrepare: TInvokePattern = cloneDeep(invokePacket) Object.keys(invokePacketForPrepare.object).forEach(key => { if (typeof invokePacketForPrepare.object[key] === 'object') invokePacketForPrepare.object[key] = undefined }) const URLs: string[] = JSON.parse(await this.client.socketRequest(SERVICE_DIRECT_INVOKE_PREPARE_EVENT, this.app.JSON.stringify(invokePacketForPrepare))) if (!this.client.accessibleURLs.size || !URLs.filter(url => this.client.accessibleURLs.has(url)).length) await this.client.refreshAccessibleURLs() const accessibleURLs: string[] = URLs.filter(url => this.client.accessibleURLs.has(url)) //判断是需要通过服务中心进行代理调用还是直接向目标发起访问调用 const requestHandler: () => Promise = async () => { return accessibleURLs.length ? new Promise((resolve, reject) => this.client.httpRequest(accessibleURLs[0], SERVICE_INVOKE_EVENT, this.app.JSON.stringify(invokePacket), invokeTimeout).then(resolve).catch(reject)) : new Promise((resolve, reject) => this.client.socketRequest(SERVICE_PROXY_INVOKE_EVENT, this.app.JSON.stringify(invokePacket), invokeTimeout).then(resolve).catch(reject)) } const responseRawPacket: string = await timeout(new Promise((resolve, reject) => requestHandler().then(resolve).catch(reject)), invokeTimeout, async () => { const timeoutException = this.generateException(ServiceInvokeTimeoutException, format(invokePacket.object), invokeTimeout) if (invokeParam.timeoutFallback !== undefined) { let fallbackResult: any if (typeof invokeParam.timeoutFallback === 'function') { fallbackResult = await invokeParam.timeoutFallback() if (fallbackResult === undefined) { throw timeoutException } } else { fallbackResult = invokeParam.timeoutFallback } invokePacket.object = fallbackResult invokePacket.chain.push(invokeParam.serviceId) return this.app.JSON.stringify(invokePacket) } else { throw timeoutException } }) const responsePacket: TInvokePattern = this.app.JSON.parse(responseRawPacket) handleObject.totalTimeSpent = responsePacket.totalTimeSpent handleObject.chain = responsePacket.chain if (responsePacket.error) { if (!responsePacket.retryableError || !responsePacket.retry) { throw responsePacket.error } else { const restRetry: number = invokeParam.retry as number invokeParam.retry = restRetry - 1 handleObject.chain.pop() return await this.invokeHandler(handleObject, invokeParam) } } let responseObject: any = responsePacket.object if (typeof responseObject === 'object' && !Array.isArray(responseObject)) { //返回数据为Object对象,进行Assign操作,否则则将对象原始内容进行返回 responseObject = Object.assign({}, invokePacket.object, responseObject) } if (invokeParam.transform) { handleObject.object = await invokeParam.transform(responseObject) } else { handleObject.object = responseObject } } /** * 调用远程方法 * @param invokeId * @param params */ public async invoke(invokeId: string | null, ...params: TInvokeObject[]): Promise { const loopObject: { invokeId: string chain: string[] object: TPattern | undefined beginAt: number totalTimeSpent: number } = { invokeId: invokeId ? invokeId : this.hyperIdInstance(), chain: [this.app.getID()], object: undefined, beginAt: Date.now(), totalTimeSpent: 0 } for (const p of params) { p.retry = p.retry === undefined ? this.retry() : p.retry await this.invokeHandler(loopObject, p) } return loopObject.object } /** * 并行调用 * @param args */ public async parallelInvoke(...args: (TInvokeObject | TInvokeObject[])[]): Promise { const id: string = this.hyperIdInstance() const parallelPromises: Promise[] = [] args.forEach(arg => { parallelPromises.push(new Promise((resolve, reject) => { if (Array.isArray(arg)) { this.invoke(id, ...arg).then(resolve).catch(reject) } else { this.invoke(id, arg).then(resolve).catch(reject) } })) }) return await Promise.all(parallelPromises) } }