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

src/index.ts   A

Complexity

Total Complexity 17
Complexity/F 2.43

Size

Lines of Code 95
Function Count 7

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 17
eloc 71
mnd 10
bc 10
fnc 7
dl 0
loc 95
ccs 35
cts 35
cp 1
rs 10
bpm 1.4285
cpm 2.4285
noi 0
c 0
b 0
f 0

7 Functions

Rating   Name   Duplication   Size   Complexity  
A index.ts ➔ isCacheValid 0 3 1
A index.ts ➔ getCachedRate 0 5 2
A index.ts ➔ getRateUrl 0 3 1
A index.ts ➔ getExchangeRate 0 20 3
A index.ts ➔ fetchRate 0 11 3
A index.ts ➔ parseRateFromResponse 0 7 5
A index.ts ➔ fetchResponse 0 7 2
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