diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b0b0a418..49da55d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Recomputed the portfolio snapshot calculation in the background on a portfolio change +- Improved the deduplication of the portfolio snapshot calculation jobs by considering the filters - Refactored the deprecated animation providers (`provideAnimations()` and `provideNoopAnimations()`) - Improved the language localization for Polish (`pl`) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index c1e795c4a..606c223b4 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -1097,15 +1097,18 @@ export abstract class PortfolioCalculator { let cachedPortfolioSnapshot: PortfolioSnapshot; let isCachedPortfolioSnapshotExpired = false; - const jobId = this.userId; + const portfolioSnapshotKey = this.redisCacheService.getPortfolioSnapshotKey( + { + filters: this.filters, + userId: this.userId + } + ); + + const jobId = portfolioSnapshotKey; try { - const cachedPortfolioSnapshotValue = await this.redisCacheService.get( - this.redisCacheService.getPortfolioSnapshotKey({ - filters: this.filters, - userId: this.userId - }) - ); + const cachedPortfolioSnapshotValue = + await this.redisCacheService.get(portfolioSnapshotKey); const { expiration, portfolioSnapshot }: PortfolioSnapshotValue = JSON.parse(cachedPortfolioSnapshotValue); diff --git a/apps/api/src/events/events.module.ts b/apps/api/src/events/events.module.ts index df943a3c9..dabc3edb7 100644 --- a/apps/api/src/events/events.module.ts +++ b/apps/api/src/events/events.module.ts @@ -1,9 +1,12 @@ import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; +import { UserModule } from '@ghostfolio/api/app/user/user.module'; +import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; +import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; import { Module } from '@nestjs/common'; @@ -13,11 +16,14 @@ import { PortfolioChangedListener } from './portfolio-changed.listener'; @Module({ imports: [ ActivitiesModule, + ApiModule, ConfigurationModule, DataGatheringQueueModule, DataProviderModule, ExchangeRateDataModule, - RedisCacheModule + PortfolioSnapshotQueueModule, + RedisCacheModule, + UserModule ], providers: [AssetProfileChangedListener, PortfolioChangedListener] }) diff --git a/apps/api/src/events/portfolio-changed.listener.ts b/apps/api/src/events/portfolio-changed.listener.ts index 12441517b..026711b93 100644 --- a/apps/api/src/events/portfolio-changed.listener.ts +++ b/apps/api/src/events/portfolio-changed.listener.ts @@ -1,4 +1,12 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { UserService } from '@ghostfolio/api/app/user/user.service'; +import { ApiService } from '@ghostfolio/api/services/api/api.service'; +import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; +import { + PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW, + PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME, + PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS +} from '@ghostfolio/common/config'; import { Injectable, Logger } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; @@ -14,7 +22,12 @@ export class PortfolioChangedListener { private debounceTimers = new Map(); - public constructor(private readonly redisCacheService: RedisCacheService) {} + public constructor( + private readonly apiService: ApiService, + private readonly portfolioSnapshotService: PortfolioSnapshotService, + private readonly redisCacheService: RedisCacheService, + private readonly userService: UserService + ) {} @OnEvent(PortfolioChangedEvent.getName()) handlePortfolioChangedEvent(event: PortfolioChangedEvent) { @@ -39,6 +52,44 @@ export class PortfolioChangedListener { private async processPortfolioChanged({ userId }: { userId: string }) { this.logger.log(`Portfolio of user '${userId}' has changed`); - await this.redisCacheService.removePortfolioSnapshotsByUserId({ userId }); + try { + await this.redisCacheService.removePortfolioSnapshotsByUserId({ userId }); + + const user = await this.userService.user({ id: userId }); + + if (!user) { + return; + } + + const userSettings = user.settings.settings; + + const filters = this.apiService.buildFiltersFromUserSettings({ + userSettings + }); + + // Recompute in the background to avoid a cold start on the next request + await this.portfolioSnapshotService.addJobToQueue({ + data: { + filters, + userId, + calculationType: userSettings.performanceCalculationType, + userCurrency: userSettings.baseCurrency + }, + name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME, + opts: { + ...PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS, + jobId: this.redisCacheService.getPortfolioSnapshotKey({ + filters, + userId + }), + priority: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW + } + }); + } catch (error) { + this.logger.error( + `Portfolio snapshot of user '${userId}' could not be recomputed`, + error + ); + } } } diff --git a/apps/api/src/services/api/api.service.ts b/apps/api/src/services/api/api.service.ts index 052119246..11074870e 100644 --- a/apps/api/src/services/api/api.service.ts +++ b/apps/api/src/services/api/api.service.ts @@ -1,4 +1,4 @@ -import { Filter } from '@ghostfolio/common/interfaces'; +import { Filter, UserSettings } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; @@ -89,4 +89,18 @@ export class ApiService { return filters; } + + public buildFiltersFromUserSettings({ + userSettings + }: { + userSettings: UserSettings; + }): Filter[] { + return this.buildFiltersFromQueryParams({ + filterByAccounts: userSettings?.['filters.accounts']?.[0], + filterByAssetClasses: userSettings?.['filters.assetClasses']?.[0], + filterByDataSource: userSettings?.['filters.dataSource'], + filterBySymbol: userSettings?.['filters.symbol'], + filterByTags: userSettings?.['filters.tags']?.[0] + }); + } }