From 3d03bf2866362164f9983d299eb6062171feed2d Mon Sep 17 00:00:00 2001 From: Andrea Bugeja Date: Mon, 18 May 2026 12:27:18 +0200 Subject: [PATCH] fix: resolve Float64Array concurrency, out of bounds, and cache invalidation race conditions --- .../roai/portfolio-calculator-cash.spec.ts | 10 +++++--- .../exchange-rate-data.service.ts | 15 +++++------ .../market-data/market-data.service.ts | 25 +++++++++++-------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index 87c08d32f..078837506 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -240,7 +240,9 @@ describe('PortfolioCalculator', () => { feeInBaseCurrency: new Big(0), grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), - grossPerformancePercentageWithCurrencyEffect: expect.any(Big), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.08211538461538461533' + ), grossPerformanceWithCurrencyEffect: new Big(70), includeInTotalAssetValue: false, investment: new Big(1820), @@ -269,8 +271,10 @@ describe('PortfolioCalculator', () => { }, quantity: new Big(2000), symbol: 'USD', - timeWeightedInvestment: expect.any(Big), - timeWeightedInvestmentWithCurrencyEffect: expect.any(Big), + timeWeightedInvestment: new Big('912.48633879781420820235'), + timeWeightedInvestmentWithCurrencyEffect: new Big( + '852.4590163934426234665' + ), valueInBaseCurrency: new Big(1820) }); diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index 3c013c2fe..3ee251ee8 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -362,7 +362,7 @@ export class ExchangeRateDataService { } private getDaysSinceEpoch(aDate: Date) { - return Math.floor(aDate.getTime() / 86400000); + return Math.floor(aDate.getTime() / 86400000) + 25569; } private async loadCache(aSymbol: string): Promise { @@ -374,7 +374,12 @@ export class ExchangeRateDataService { }); const todayDays = this.getDaysSinceEpoch(new Date()); - const array = new Float64Array(todayDays + 1); + const maxDataDays = + marketData.length > 0 + ? this.getDaysSinceEpoch(marketData[marketData.length - 1].date) + : 0; + + const array = new Float64Array(Math.max(todayDays, maxDataDays) + 1); if (marketData.length > 0) { let lastRate = marketData[0].marketPrice; @@ -404,14 +409,10 @@ export class ExchangeRateDataService { ): Promise { let cache = this.exchangeRateCache.get(aSymbol); - if (!cache) { + while (!cache) { if (this.pendingLoads.has(aSymbol)) { await this.pendingLoads.get(aSymbol); cache = this.exchangeRateCache.get(aSymbol); - - if (!cache) { - cache = await this.loadAndCommit(aSymbol); - } } else { cache = await this.loadAndCommit(aSymbol); } diff --git a/apps/api/src/services/market-data/market-data.service.ts b/apps/api/src/services/market-data/market-data.service.ts index a8ba4e709..ea1d630e1 100644 --- a/apps/api/src/services/market-data/market-data.service.ts +++ b/apps/api/src/services/market-data/market-data.service.ts @@ -22,13 +22,16 @@ export class MarketDataService { ) {} public async deleteMany({ dataSource, symbol }: AssetProfileIdentifier) { - this.eventEmitter.emit('market-data.updated', { symbol }); - return this.prismaService.marketData.deleteMany({ + const result = await this.prismaService.marketData.deleteMany({ where: { dataSource, symbol } }); + + this.eventEmitter.emit('market-data.updated', { symbol }); + + return result; } public async get({ @@ -198,14 +201,7 @@ export class MarketDataService { oldAssetProfileIdentifier: AssetProfileIdentifier, newAssetProfileIdentifier: AssetProfileIdentifier ) { - this.eventEmitter.emit('market-data.updated', { - symbol: oldAssetProfileIdentifier.symbol - }); - this.eventEmitter.emit('market-data.updated', { - symbol: newAssetProfileIdentifier.symbol - }); - - return this.prismaService.marketData.updateMany({ + const result = await this.prismaService.marketData.updateMany({ data: { dataSource: newAssetProfileIdentifier.dataSource, symbol: newAssetProfileIdentifier.symbol @@ -215,6 +211,15 @@ export class MarketDataService { symbol: oldAssetProfileIdentifier.symbol } }); + + this.eventEmitter.emit('market-data.updated', { + symbol: oldAssetProfileIdentifier.symbol + }); + this.eventEmitter.emit('market-data.updated', { + symbol: newAssetProfileIdentifier.symbol + }); + + return result; } public async updateMarketData(params: {