dm-web-react
Version:
The DM web client with React.
381 lines (342 loc) • 16.9 kB
text/typescript
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
}