UNPKG

dm-web-react

Version:

The DM web client with React.

381 lines (342 loc) 16.9 kB
import * as _ from "lodash"; import BackgroundSharedWorker from "shared-worker-loader!../../../workers/backgroundSharedWorker"; import { DynamicQuery } from "ts-dynamic-query"; import { BrokerMarketBestPriceView } from "../models/entity/view/brokerMarketBestPriceView"; import { DmLiteDatabaseService } from "../../../services/dmLiteDatabaseService"; import { ArrayUtils, ObjectUtils, DateUtils } from "ts-commons"; import { PageDto } from "../../../models/dto/pageDto"; import { WorkerMessage } from "../../../models/workerMessage/workerMessage"; import { WorkerMessageType } from "../../../models/enum/workerMessageType"; import { SocketIoInnodealingMessage } from "../../../models/socketIo/socketIoInnodealingMessage"; import { SocketIoEventMessage } from "../../../models/socketIo/socketIoEventMessage"; import { plainToClass } from "class-transformer"; import { DmLiteMessageService } from "../../../services/dmLiteMessageService"; import { BrokerMarketBestPrice } from "../models/entity/table"; import { InnodealingMessageType } from "../../../models/constants"; import { SocketIoInnodealingWorkerMessage } from "../../../models/workerMessage/socketIoInnodealingWorkerMessage"; import { SocketIoEventWorkerMessage } from "../../../models/workerMessage/socketIoEventWorkerMessage"; import { SocketIoEventType } from "../../../models/enum/socketIoEventType"; import { PreloadingHeartbeatWorkerMessage } from "../../../models/workerMessage/preloadingHeartbeatWorkerMessage"; import { MessagePortHeartbeatWorkerMessage } from "../../../models/workerMessage/messagePortHeartbeatWorkerMessage"; import { BrokerMarketBestPriceDto, fromEntity } from "../models/dto/brokerMarketBestPriceDto"; export interface SocketIoCacheConfig { cacheCount: number; queryCache: DynamicQuery<BrokerMarketBestPriceView>; newDataCallback: (newData: BrokerMarketBestPriceDto[]) => void; } export interface DmLiteBrokerMarketBusinessConfig { browserId: string; refreshData?: () => void; notifyPreloadingOffline?: () => void; } export class DmLiteBrokerMarketBusiness { private readonly backgroundSharedWorkerMessagePort: MessagePort; private readonly dmLiteMessageService: DmLiteMessageService; private readonly dmLiteDatabaseService: DmLiteDatabaseService; private readonly browserId: string; private socketIoDataCache: BrokerMarketBestPriceView[] = []; private socketIoCacheConfig?: SocketIoCacheConfig; private newBestPrices: Map<string, BrokerMarketBestPrice>; private needShowHighlightBestPrices: Map<string, Date>; private notifyNewDataEvent?: (data: string) => void; private refreshData?: () => void; private notifyPreloadingOffline?: () => void; constructor(config: DmLiteBrokerMarketBusinessConfig) { this.browserId = config.browserId; this.refreshData = config.refreshData; this.notifyPreloadingOffline = config.notifyPreloadingOffline; this.newBestPrices = new Map<string, BrokerMarketBestPrice>(); this.needShowHighlightBestPrices = new Map<string, Date>(); this.dmLiteDatabaseService = DmLiteDatabaseService.Instance; this.dmLiteMessageService = DmLiteMessageService.Instance; const backgroundSharedWorker = new BackgroundSharedWorker(); this.backgroundSharedWorkerMessagePort = backgroundSharedWorker.port; this.backgroundSharedWorkerMessagePort.addEventListener("message", evt => { const data = evt.data as WorkerMessage; if (data.messageType === WorkerMessageType.SOCKETIO_INNODEALING_MESSAGE) { const message = plainToClass<SocketIoInnodealingWorkerMessage, object>(SocketIoInnodealingWorkerMessage, data); const innodealingMessage = plainToClass<SocketIoInnodealingMessage, object>(SocketIoInnodealingMessage, message.message); this.handleSocketIoInnodealingMessage(innodealingMessage); } else if (data.messageType === WorkerMessageType.SOCKETIO_EVENT_MESSAGE) { const message = plainToClass<SocketIoEventWorkerMessage, object>(SocketIoEventWorkerMessage, data); const eventMessage = plainToClass<SocketIoEventMessage, object>(SocketIoEventMessage, message.message); this.handleSocketIoEventMessage(eventMessage); } else if (data.messageType === WorkerMessageType.PRELOADING_HEARTBEAT) { const message = plainToClass<PreloadingHeartbeatWorkerMessage, object>(PreloadingHeartbeatWorkerMessage, data); this.handlePerloadingHeartbeat(message); } }); this.start(); } public start(): void { this.backgroundSharedWorkerMessagePort.start(); this.startPolling(); } public stop(): void { this.backgroundSharedWorkerMessagePort.close(); this.stopPolling(); } public isLocalDbVersionEqual() { return this.dmLiteDatabaseService.isLocalDbVersionEqual(this.browserId); } public async getBrokerMarketBestPricePageDto( dynamicQuery: DynamicQuery<BrokerMarketBestPriceView>, offset: number, limit: number ): Promise<PageDto<BrokerMarketBestPriceDto>> { try { console.log("getBrokerMarketBestPricePageDto start"); const result = await this.dmLiteDatabaseService.getBrokerMarketBestPriceViewWithTotalCount(this.browserId, dynamicQuery, offset, limit); const pageDto = new PageDto<BrokerMarketBestPriceDto>(); pageDto.data = _.map(result.data, x => fromEntity(x)); pageDto.totalCount = result.totalCount; pageDto.offset = offset; pageDto.limit = limit; console.log("getBrokerMarketBestPricePageDto end"); return new Promise<PageDto<BrokerMarketBestPriceDto>>(resolve => resolve(pageDto)); } catch (e) { console.error(e); this.pushError(`getBrokerMarketBestPricePageDto error: ${e.message || JSON.stringify(e)}`); return new Promise<PageDto<BrokerMarketBestPriceDto>>((resolve, reject) => reject(e)); } } public async getBrokerMarketBestPriceDto( dynamicQuery: DynamicQuery<BrokerMarketBestPriceView>, offset: number, limit: number ): Promise<BrokerMarketBestPriceDto[]> { try { console.log("getBrokerMarketBestPriceDto start"); const data = await this.dmLiteDatabaseService.getBrokerMarketBestPriceView(this.browserId, dynamicQuery, offset, limit); const result = _.map(data, x => fromEntity(x)); console.log("getBrokerMarketBestPriceDto end"); return new Promise<BrokerMarketBestPriceDto[]>(resolve => resolve(result)); } catch (e) { console.error(e); this.pushError(`getBrokerMarketBestPriceDto error: ${e.message || JSON.stringify(e)}`); return new Promise<BrokerMarketBestPriceDto[]>((resolve, reject) => reject(e)); } } public async getBrokerMarketBestPriceView( dynamicQuery: DynamicQuery<BrokerMarketBestPriceView>, offset: number, limit: number ): Promise<BrokerMarketBestPriceView[]> { try { const data = await this.dmLiteDatabaseService.getBrokerMarketBestPriceView(this.browserId, dynamicQuery, offset, limit); return new Promise<BrokerMarketBestPriceView[]>(resolve => resolve(data)); } catch (e) { console.error(e); this.pushError(`getBrokerMarketBestPriceDto error: ${e.message || JSON.stringify(e)}`); return new Promise<BrokerMarketBestPriceView[]>((resolve, reject) => reject(e)); } } public async getBrokerMarketBestPriceTotalCount(dynamicQuery: DynamicQuery<BrokerMarketBestPriceView>): Promise<number> { try { const result = await this.dmLiteDatabaseService.getBrokerMarketBestPriceCount(this.browserId, dynamicQuery); return new Promise<number>(resolve => resolve(result)); } catch (e) { console.error(e); this.pushError(`getBrokerMarketBestPriceTotalCount error: ${e.message || JSON.stringify(e)}`); return new Promise<number>((resolve, reject) => reject(e)); } } public async setSocketIoEnable(config: SocketIoCacheConfig): Promise<void> { try { console.log("setSocketIoEnable start"); this.notifyNewDataEvent = undefined; // no need notify new day. this.socketIoCacheConfig = config; // clone one. this.socketIoCacheConfig.queryCache = new DynamicQuery<BrokerMarketBestPriceView>().fromJSON(config.queryCache.toJSON()); this.socketIoDataCache = await this.getBrokerMarketBestPriceView(config.queryCache, 0, config.cacheCount); console.log(`cache data count: ${this.socketIoDataCache.length}`); } catch (e) { this.pushError(`setSocketIoEnable error: ${e.message || JSON.stringify(e)}`); } finally { console.log("setSocketIoEnable end"); } } public setSocketIoDisable(notifyNewDataEvent: (dataId: string) => void) { this.notifyNewDataEvent = notifyNewDataEvent; this.socketIoCacheConfig = undefined; // no need update ui. } private handleSocketIoInnodealingMessage(message: SocketIoInnodealingMessage): void { console.log("handleSocketIoInnodealingMessage: ", message.messageId); if (message.messageType === InnodealingMessageType.BROKER_MARKET_BEST_PRICE) { const newBestPrice = plainToClass<BrokerMarketBestPrice, object>(BrokerMarketBestPrice, message.data); if (this.socketIoCacheConfig) { this.needShowHighlightBestPrices.set(newBestPrice.dataId, new Date()); this.newBestPrices.set(newBestPrice.dataId, newBestPrice); } if (this.notifyNewDataEvent) { this.notifyNewDataEvent(newBestPrice.dataId); } } } private handleSocketIoEventMessage(message: SocketIoEventMessage): void { if (message.eventType === SocketIoEventType.CONNECT) { console.info("handleSocketIoEventMessage CONNECT!"); } else if (message.eventType === SocketIoEventType.DISCONNECT) { console.warn("handleSocketIoEventMessage DISCONNECT!"); this.stopPolling(); } else if (message.eventType === SocketIoEventType.RECONNECT) { console.info("handleSocketIoEventMessage RECONNECT!"); if (!ObjectUtils.isNullOrUndefined(this.refreshData)) { this.refreshData!(); } this.startPolling(); } else if (message.eventType === SocketIoEventType.ERROR) { console.error(`handleSocketIoEventMessage error: ${message.error}`); } } private isDoPushBusy = false; private doPushAction = async (): Promise<void> => { try { this.isDoPushBusy = true; if (!this.socketIoCacheConfig || ObjectUtils.isNullOrUndefined(this.socketIoCacheConfig.newDataCallback)) { return new Promise<void>(resolve => resolve()); } if (this.newBestPrices.size === 0) { return new Promise<void>(resolve => resolve()); } console.log("doPushAction start"); const newPrices = Array.from(this.newBestPrices.values()); const useNewBestPrices = _.takeRight(newPrices, this.socketIoCacheConfig.cacheCount); const bondUniCodes = _.map(useNewBestPrices, x => x.bondUniCode); console.log("doPushAction get bondUniCodes length: ", bondUniCodes.length); const bondInfos = await this.dmLiteDatabaseService.getBondInfosByBondUniCodes(this.browserId, bondUniCodes); console.log("doPushAction get bondInfo length: ", bondInfos.length); const validBondUnicodes = _.map(bondInfos, x => x.bondUniCode); const notFoundUnicodes = _.differenceWith(bondUniCodes, validBondUnicodes, _.isEqual); console.log("dnotFoundUnicodes: ", notFoundUnicodes); const pushDataIds: string[] = []; const newDataView: BrokerMarketBestPriceView[] = []; for (const newItem of useNewBestPrices) { const matchBondInfo = _.find(bondInfos, { bondUniCode: newItem.bondUniCode }); if (matchBondInfo) { pushDataIds.push(newItem.dataId); const view = _.assign(new BrokerMarketBestPriceView(), matchBondInfo, newItem); newDataView.push(view); } } const needPushItems = this.socketIoCacheConfig.queryCache.query(newDataView); if (ArrayUtils.isEmpty(needPushItems)) { // no items need push to UI. console.log("no items need push to UI"); return new Promise<void>(resolve => resolve()); } const noPushDataIdItems = _.filter(this.socketIoDataCache, x => !ArrayUtils.contains(pushDataIds, x.dataId)); const removeDuplicatedCache = noPushDataIdItems.concat(newDataView); this.socketIoDataCache = _.take(this.socketIoCacheConfig.queryCache.query(removeDuplicatedCache), this.socketIoCacheConfig.cacheCount); const result = _.map(this.socketIoDataCache, x => { const dto = fromEntity(x); dto.isNew = this.needShowHighlightBestPrices.has(dto.dataId); return dto; }); this.socketIoCacheConfig.newDataCallback(result); this.newBestPrices.clear(); // clear return new Promise<void>(resolve => resolve()); } catch (e) { console.error(e); return new Promise<void>((resolve, reject) => reject(e)); } finally { this.isDoPushBusy = false; } }; private pushError(errMsg: string) { this.dmLiteMessageService.pushError(this.browserId, errMsg); } //#region polling private refestNeedShowHightTickHandle: number; private checkBackgroundWorkerTickHandle: number; private heartbeatTickerHandle: number; private doPushActionTickerHandle: number; private clearOldCacheTickerHandle: number; private startPolling(): void { console.log("startPolling start"); this.stopPolling(); this.refestNeedShowHightTickHandle = window.setInterval(() => { this.refreshNeedShowHighlightBestPrices(); }, 1000); this.checkBackgroundWorkerTickHandle = window.setInterval(() => { this.checkPreloadingHeartbeatTicker(); }, 30000); this.heartbeatTickerHandle = window.setInterval(() => { this.sendMessagePortHeartbeat(); }, 10000); this.doPushActionTickerHandle = window.setInterval(() => { if (this.isDoPushBusy) { console.warn("isDoPushBusy!!!"); } else { this.doPushAction(); } }, 500); this.clearOldCacheTickerHandle = window.setInterval(() => { this.clearOldCache(); }, 1000 * 60 * 60); console.log("startPolling end"); } private stopPolling(): void { console.log("stopPolling start"); window.clearInterval(this.refestNeedShowHightTickHandle); window.clearInterval(this.checkBackgroundWorkerTickHandle); window.clearInterval(this.heartbeatTickerHandle); window.clearInterval(this.doPushActionTickerHandle); window.clearInterval(this.clearOldCacheTickerHandle); console.log("stopPolling end"); } private lastToday: Date; private clearOldCache(): void { console.log("clearOldCache start"); const today = DateUtils.getToday(); const todayTimestamp = DateUtils.dateToTimestamp(today); this.socketIoDataCache = _.filter(this.socketIoDataCache, x => { return x.issueDataTime > todayTimestamp; }); if ( !ObjectUtils.isNullOrUndefined(this.refreshData) && !ObjectUtils.isNullOrUndefined(this.lastToday) && this.lastToday.getTime() !== today.getTime() ) { this.refreshData!(); } this.lastToday = today; console.log("clearOldCache end"); } // check new data whether need high light new price private refreshNeedShowHighlightBestPrices(): void { const now = new Date(); const needRemoveKeys: string[] = []; this.needShowHighlightBestPrices.forEach((value, key) => { if (now.getTime() - value.getTime() > 5000) { needRemoveKeys.push(key); } }); for (const key of needRemoveKeys) { this.needShowHighlightBestPrices.delete(key); } } private sendMessagePortHeartbeat(): void { const heartbeat = new MessagePortHeartbeatWorkerMessage(); this.backgroundSharedWorkerMessagePort.postMessage(heartbeat); console.log("sendMessagePortHeartbeat: ", heartbeat.messageId); } //#endregion //#region check shared worker exists private lastCheckPreloadingHeartbeatTime: Date = new Date(); private checkPreloadingHeartbeatTicker(): void { if (new Date().getTime() - this.lastCheckPreloadingHeartbeatTime.getTime() > 60000) { console.error("preloading not exist"); if (!ObjectUtils.isNullOrUndefined(this.notifyPreloadingOffline)) { this.notifyPreloadingOffline!(); } } else { console.log("preloading works"); } } private handlePerloadingHeartbeat(response: PreloadingHeartbeatWorkerMessage): void { this.lastCheckPreloadingHeartbeatTime = new Date(); console.log("handlePerloadingHeartbeat: ", response.messageId); } //#endregion }