perf(storage): optimize historical rates batch fetching

Refactored `fetchHistoricalRatesBatch` to use a single range-based batch API query to Frankfurter (`minDate..maxDate`), replacing the N+1 `Promise.all` logic for web environment fallback. Also fixed handling of weekend boundary limits by querying an extra 7 days prior to the first requested date to resolve closest previous available rates.

Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot] 2026-08-26 14:10:29 +00:00
parent 37cfd2144b
commit b7aea8b3ea
2 changed files with 61 additions and 7 deletions

View file

@ -1,6 +1,6 @@
import { logger } from "./logger"; import { logger } from "./logger";
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { isTauri, loadSettings, saveSettings, defaultSettingsData, loadAppData, saveAppData, flushAppData, setDataDir, runBackups, backupDeletedSheet, openBackupsFolder, chooseFolder, dataDirHasDocuments, migrateDataDir, fetchRates, fetchHistoricalRates } from './storage'; import { isTauri, loadSettings, saveSettings, defaultSettingsData, loadAppData, saveAppData, flushAppData, setDataDir, runBackups, backupDeletedSheet, openBackupsFolder, chooseFolder, dataDirHasDocuments, migrateDataDir, fetchRates, fetchHistoricalRates, fetchHistoricalRatesBatch } from './storage';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
vi.mock('@tauri-apps/api/core', () => ({ vi.mock('@tauri-apps/api/core', () => ({
@ -382,6 +382,39 @@ describe('storage', () => {
}); });
}); });
describe('fetchHistoricalRatesBatch', () => {
it('uses invoke when in Tauri', async () => {
vi.stubGlobal('window', { __TAURI_INTERNALS__: {} });
const mockRates = { '2024-01-01': { EUR: 0.85 } };
vi.mocked(invoke).mockResolvedValue(mockRates);
const result = await fetchHistoricalRatesBatch(['2024-01-01']);
expect(invoke).toHaveBeenCalledWith('fetch_historical_rates_batch', { dates: ['2024-01-01'] });
expect(result).toEqual(mockRates);
});
it('uses fetch when not in Tauri and falls back to closest dates', async () => {
const mockResponse = { rates: { '2023-12-29': { EUR: 0.84 }, '2024-01-02': { EUR: 0.85, JPY: -1 }, '2024-01-05': { EUR: 0.86 } } };
vi.mocked(fetch).mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) } as unknown as Response);
const dates = ['2024-01-01', '2024-01-03', '2024-01-05'];
const result = await fetchHistoricalRatesBatch(dates);
expect(fetch).toHaveBeenCalledWith('https://api.frankfurter.dev/v1/2023-12-25..2024-01-05?from=USD');
expect(result['2024-01-01']).toEqual({ USD: 1, EUR: 0.84 });
expect(result['2024-01-03']).toEqual({ USD: 1, EUR: 0.85 });
expect(result['2024-01-05']).toEqual({ USD: 1, EUR: 0.86 });
});
it('returns empty if fetch fails', async () => {
vi.mocked(fetch).mockRejectedValue(new Error('Network error'));
const result = await fetchHistoricalRatesBatch(['2024-01-01']);
expect(logger.warn).toHaveBeenCalledWith('fetchHistoricalRatesBatch failed', expect.any(Error));
expect(result).toEqual({});
});
});
describe('fetchHistoricalRates', () => { describe('fetchHistoricalRates', () => {
it('uses invoke when in Tauri', async () => { it('uses invoke when in Tauri', async () => {
vi.stubGlobal('window', { __TAURI_INTERNALS__: {} }); vi.stubGlobal('window', { __TAURI_INTERNALS__: {} });

View file

@ -219,13 +219,34 @@ export async function fetchHistoricalRatesBatch(dates: string[]): Promise<Record
if (isTauri()) { if (isTauri()) {
return await invoke<Record<string, Record<string, number>>>("fetch_historical_rates_batch", { dates }); return await invoke<Record<string, Record<string, number>>>("fetch_historical_rates_batch", { dates });
} }
const sortedDates = [...dates].sort();
const minDateObj = new Date(sortedDates[0]);
minDateObj.setDate(minDateObj.getDate() - 7);
const minDateQuery = minDateObj.toISOString().split("T")[0];
const maxDate = sortedDates[sortedDates.length - 1];
const res = await fetch(`https://api.frankfurter.dev/v1/${minDateQuery}..${maxDate}?from=USD`);
const json = await res.json();
if (!json?.rates) return {};
const rangeDates = Object.keys(json.rates).sort();
const results: Record<string, Record<string, number>> = {}; const results: Record<string, Record<string, number>> = {};
await Promise.all(
dates.map(async (date) => { for (const date of dates) {
const rates = await fetchHistoricalRates(date); let targetDate = date;
if (rates) results[date] = rates; if (!json.rates[date]) {
}) const possibleDates = rangeDates.filter(d => d <= date);
); if (possibleDates.length === 0) continue;
targetDate = possibleDates[possibleDates.length - 1];
}
const dayRates = json.rates[targetDate] as Record<string, number>;
const out: Record<string, number> = { USD: 1 };
for (const [k, v] of Object.entries(dayRates)) {
if (v > 0) out[k.toUpperCase()] = v;
}
results[date] = out;
}
return results; return results;
} catch (e) { } catch (e) {
logger.warn("fetchHistoricalRatesBatch failed", e); logger.warn("fetchHistoricalRatesBatch failed", e);