Passed
Push — main ( 5f1504...6fb199 )
by Yuri
01:20
created

index.ts ➔ fetchRate   A

Complexity

Conditions 3

Size

Total Lines 11
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 9.9
c 0
b 0
f 0
cc 3
crap 3
1 2
import { BackendError, FetchError, MalformedError } from './errors'
2
3
export type CurrencyCode = string;
4
5
interface YFResponse {
6
  chart: {
7
    result: {
8
      meta: {
9
        regularMarketPrice: number
10
      }
11
    }[]
12
  }
13
}
14
15
interface CachedRate {
16
  value: number;
17
  timestamp: number;
18
}
19
20
interface ExchangeRateOptions {
21
  cacheDurationMs?: number; // Defaults to undefined (no caching)
22
}
23
24 2
const YF_BASE = 'https://query1.finance.yahoo.com/v8/finance/chart/'
25 2
const YF_PARAMS = '=X?region=US&lang=en-US&includePrePost=false&interval=2m&useYfid=true&range=1d&corsDomain=finance.yahoo.com&.tsrc=finance'
26
27 2
const rateCache: Record<string, CachedRate> = {}
28
29
function getRateUrl (from: CurrencyCode, to: CurrencyCode): string {
30 13
  return `${YF_BASE}${from.toUpperCase()}${to.toUpperCase()}${YF_PARAMS}`
31
}
32
33
function isCacheValid (cachedRate: CachedRate, cacheDurationMs: number): boolean {
34 3
  return Date.now() - cachedRate.timestamp < cacheDurationMs
35
}
36
37
function getCachedRate (from: CurrencyCode, to: CurrencyCode, cacheDurationMs?: number): number | null {
38 14
  const cacheKey = `${from}-${to}`
39 14
  const cachedRate = rateCache[cacheKey]
40 14
  return (cacheDurationMs && cachedRate && isCacheValid(cachedRate, cacheDurationMs)) ? cachedRate.value : null
41
}
42
43
async function fetchResponse (rateUrl: string): Promise<Response> {
44 13
  const response = await fetch(rateUrl)
45 12
  if (!response.ok) {
46 2
    throw new BackendError(`Service did not return HTTP 200 response. Status: ${response.status}`)
47
  }
48 10
  return response
49
}
50
51
function parseRateFromResponse (result: YFResponse): number {
52 10
  const rate = result.chart?.result[0]?.meta?.regularMarketPrice
53 10
  if (!rate) {
54 1
    throw new MalformedError('Unexpected response structure. Missing "regularMarketPrice".')
55
  }
56 9
  return rate
57
}
58
59
async function fetchRate (rateUrl: string): Promise<number> {
60 13
  try {
61 13
    const response = await fetchResponse(rateUrl)
62 10
    const responseData: YFResponse = await response.json()
63 10
    return parseRateFromResponse(responseData)
64
  } catch (error) {
65 4
    if (error instanceof BackendError || error instanceof MalformedError) {
66 3
      throw error
67
    }
68 1
    throw new FetchError(`Failed to fetch data from ${rateUrl}`)
69
  }
70
}
71
72 2
export async function getExchangeRate (
73
  from: CurrencyCode,
74
  to: CurrencyCode,
75
  options: ExchangeRateOptions = {}
76
): Promise<number> {
77 14
  const cachedRate = getCachedRate(from, to, options.cacheDurationMs)
78 14
  if (cachedRate !== null) {
79 1
    return cachedRate
80
  }
81
82 13
  const rateUrl = getRateUrl(from, to)
83 13
  const rate = await fetchRate(rateUrl)
84
85 9
  if (options.cacheDurationMs) {
86 3
    const cacheKey = `${from}-${to}`
87 3
    rateCache[cacheKey] = { value: rate, timestamp: Date.now() } // Cache the result with a timestamp
88
  }
89
90 9
  return rate
91
}
92
93
94
export * from './errors'
95