Browse Source

Task/debounce portfolio and asset profile change listeners (#6505)

* Debounce portfolio and asset profile change event listeners

* Update changelog
pull/6501/head
Thomas Kaul 2 weeks ago
committed by GitHub
parent
commit
41f0dfda65
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 12
      apps/api/src/events/asset-profile-changed.event.ts
  3. 59
      apps/api/src/events/asset-profile-changed.listener.ts
  4. 30
      apps/api/src/events/portfolio-changed.listener.ts

4
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added a debounce to the `PortfolioChangedListener` and `AssetProfileChangedListener` to minimize redundant _Redis_ and database operations
### Changed ### Changed
- Improved the _Storybook_ stories of the value component - Improved the _Storybook_ stories of the value component

12
apps/api/src/events/asset-profile-changed.event.ts

@ -8,4 +8,16 @@ export class AssetProfileChangedEvent {
public static getName(): string { public static getName(): string {
return 'assetProfile.changed'; return 'assetProfile.changed';
} }
public getCurrency() {
return this.data.currency;
}
public getDataSource() {
return this.data.dataSource;
}
public getSymbol() {
return this.data.symbol;
}
} }

59
apps/api/src/events/asset-profile-changed.listener.ts

@ -4,14 +4,21 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import { DataSource } from '@prisma/client';
import ms from 'ms';
import { AssetProfileChangedEvent } from './asset-profile-changed.event'; import { AssetProfileChangedEvent } from './asset-profile-changed.event';
@Injectable() @Injectable()
export class AssetProfileChangedListener { export class AssetProfileChangedListener {
private static readonly DEBOUNCE_DELAY = ms('5 seconds');
private debounceTimers = new Map<string, NodeJS.Timeout>();
public constructor( public constructor(
private readonly activitiesService: ActivitiesService, private readonly activitiesService: ActivitiesService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
@ -21,9 +28,47 @@ export class AssetProfileChangedListener {
) {} ) {}
@OnEvent(AssetProfileChangedEvent.getName()) @OnEvent(AssetProfileChangedEvent.getName())
public async handleAssetProfileChanged(event: AssetProfileChangedEvent) { public handleAssetProfileChanged(event: AssetProfileChangedEvent) {
const currency = event.getCurrency();
const dataSource = event.getDataSource();
const symbol = event.getSymbol();
const key = getAssetProfileIdentifier({
dataSource,
symbol
});
const existingTimer = this.debounceTimers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
this.debounceTimers.set(
key,
setTimeout(() => {
this.debounceTimers.delete(key);
void this.processAssetProfileChanged({
currency,
dataSource,
symbol
});
}, AssetProfileChangedListener.DEBOUNCE_DELAY)
);
}
private async processAssetProfileChanged({
currency,
dataSource,
symbol
}: {
currency: string;
dataSource: DataSource;
symbol: string;
}) {
Logger.log( Logger.log(
`Asset profile of ${event.data.symbol} (${event.data.dataSource}) has changed`, `Asset profile of ${symbol} (${dataSource}) has changed`,
'AssetProfileChangedListener' 'AssetProfileChangedListener'
); );
@ -31,16 +76,16 @@ export class AssetProfileChangedListener {
this.configurationService.get( this.configurationService.get(
'ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES' 'ENABLE_FEATURE_GATHER_NEW_EXCHANGE_RATES'
) === false || ) === false ||
event.data.currency === DEFAULT_CURRENCY currency === DEFAULT_CURRENCY
) { ) {
return; return;
} }
const existingCurrencies = this.exchangeRateDataService.getCurrencies(); const existingCurrencies = this.exchangeRateDataService.getCurrencies();
if (!existingCurrencies.includes(event.data.currency)) { if (!existingCurrencies.includes(currency)) {
Logger.log( Logger.log(
`New currency ${event.data.currency} has been detected`, `New currency ${currency} has been detected`,
'AssetProfileChangedListener' 'AssetProfileChangedListener'
); );
@ -48,13 +93,13 @@ export class AssetProfileChangedListener {
} }
const { dateOfFirstActivity } = const { dateOfFirstActivity } =
await this.activitiesService.getStatisticsByCurrency(event.data.currency); await this.activitiesService.getStatisticsByCurrency(currency);
if (dateOfFirstActivity) { if (dateOfFirstActivity) {
await this.dataGatheringService.gatherSymbol({ await this.dataGatheringService.gatherSymbol({
dataSource: this.dataProviderService.getDataSourceForExchangeRates(), dataSource: this.dataProviderService.getDataSourceForExchangeRates(),
date: dateOfFirstActivity, date: dateOfFirstActivity,
symbol: `${DEFAULT_CURRENCY}${event.data.currency}` symbol: `${DEFAULT_CURRENCY}${currency}`
}); });
} }
} }

30
apps/api/src/events/portfolio-changed.listener.ts

@ -2,22 +2,44 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import ms from 'ms';
import { PortfolioChangedEvent } from './portfolio-changed.event'; import { PortfolioChangedEvent } from './portfolio-changed.event';
@Injectable() @Injectable()
export class PortfolioChangedListener { export class PortfolioChangedListener {
private static readonly DEBOUNCE_DELAY = ms('5 seconds');
private debounceTimers = new Map<string, NodeJS.Timeout>();
public constructor(private readonly redisCacheService: RedisCacheService) {} public constructor(private readonly redisCacheService: RedisCacheService) {}
@OnEvent(PortfolioChangedEvent.getName()) @OnEvent(PortfolioChangedEvent.getName())
handlePortfolioChangedEvent(event: PortfolioChangedEvent) { handlePortfolioChangedEvent(event: PortfolioChangedEvent) {
const userId = event.getUserId();
const existingTimer = this.debounceTimers.get(userId);
if (existingTimer) {
clearTimeout(existingTimer);
}
this.debounceTimers.set(
userId,
setTimeout(() => {
this.debounceTimers.delete(userId);
void this.processPortfolioChanged({ userId });
}, PortfolioChangedListener.DEBOUNCE_DELAY)
);
}
private async processPortfolioChanged({ userId }: { userId: string }) {
Logger.log( Logger.log(
`Portfolio of user '${event.getUserId()}' has changed`, `Portfolio of user '${userId}' has changed`,
'PortfolioChangedListener' 'PortfolioChangedListener'
); );
this.redisCacheService.removePortfolioSnapshotsByUserId({ await this.redisCacheService.removePortfolioSnapshotsByUserId({ userId });
userId: event.getUserId()
});
} }
} }

Loading…
Cancel
Save