mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 01:02:01 +03:00
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:
parent
37cfd2144b
commit
b7aea8b3ea
2 changed files with 61 additions and 7 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { logger } from "./logger";
|
||||
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';
|
||||
|
||||
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', () => {
|
||||
it('uses invoke when in Tauri', async () => {
|
||||
vi.stubGlobal('window', { __TAURI_INTERNALS__: {} });
|
||||
|
|
|
|||
|
|
@ -219,13 +219,34 @@ export async function fetchHistoricalRatesBatch(dates: string[]): Promise<Record
|
|||
if (isTauri()) {
|
||||
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>> = {};
|
||||
await Promise.all(
|
||||
dates.map(async (date) => {
|
||||
const rates = await fetchHistoricalRates(date);
|
||||
if (rates) results[date] = rates;
|
||||
})
|
||||
);
|
||||
|
||||
for (const date of dates) {
|
||||
let targetDate = date;
|
||||
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;
|
||||
} catch (e) {
|
||||
logger.warn("fetchHistoricalRatesBatch failed", e);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue