3.9 KiB
rate-cache — TTL-bounded rate cache with triangulated lookup
(no submodules)
Purpose
RateCache is the converter's in-memory rate store. It folds every successful
fetchRates answer into a single map keyed by base currency, evicts entries
older than the configured TTL, and resolves rates for any pair through one of
four paths: identity (from === to), direct (cache has a from base with a
to rate), inverse (cache has a to base with a from rate — return
1 / rate), or triangulated (any cached non-from/to base contains both
from and to). The cache is universal: only Date.now, Map, and
plain object access.
The cache is a pure storage substrate — it does not call providers, does not
seed virtual rates, and does not raise typed errors. Those concerns live in
CurrencyConverter. Keeping the cache concern-free makes it cheap to swap
for a persistent adapter in a follow-up change (see proposal ## Out of scope).
Features
- TTL-bounded entries (
setrecordsDate.now() + ttlMsasexpiresAt). - Lazy eviction — expired entries are deleted on
get/has. getRate(from, to)resolves identity, direct, inverse, and triangulated matches.getTriangulationPath(from, to)surfaces the intermediate base used for triangulation (orundefinedfor direct / inverse / identity / miss).prune()removes every expired entry and returns the count removed.clear(),size,getTimestamp(base)round out the inspection surface.- Skips zero rates during inverse / triangulation lookup to avoid
Infinity.
Usage: Basic Example
import { RateCache } from "@suaveplan/currency";
const cache = new RateCache(60_000); // 1-minute TTL
cache.set("USD", { base: "USD", rates: { EUR: 0.92 }, timestamp: Date.now() });
cache.getRate("USD", "EUR"); // 0.92
cache.getRate("EUR", "USD"); // 1 / 0.92 (inverse)
cache.getRate("USD", "USD"); // 1 (identity)
Usage: Advanced Example
Use the triangulation path to log which intermediate base resolved a quote:
import { RateCache } from "@suaveplan/currency";
const cache = new RateCache();
cache.set("USD", {
base: "USD",
rates: { EUR: 0.5, JPY: 100 },
timestamp: Date.now(),
});
const rate = cache.getRate("EUR", "JPY"); // 200 (100 / 0.5)
const through = cache.getTriangulationPath("EUR", "JPY"); // "USD"
console.log(`EUR → JPY via ${through} = ${rate}`);
API Reference
| Member | Kind | Description |
|---|---|---|
new RateCache(ttlMs?) |
constructor | ttlMs defaults to 3_600_000 (1 hour). |
set(base, result) |
method | Stores a RateFetchResult and stamps expiresAt. |
get(base) |
method | Returns the cached RateFetchResult or undefined; deletes expired entries. |
has(base) |
method | get(base) !== undefined. |
getRate(from, to) |
method | Number for identity / direct / inverse / triangulated matches, otherwise undefined. |
getTriangulationPath(from, to) |
method | The intermediate base when triangulation hit, otherwise undefined. |
getTimestamp(base) |
method | result.timestamp of the fresh entry; undefined when missing or expired. |
prune() |
method | Deletes every expired entry; returns the count removed. |
clear() |
method | Empties the cache. |
size |
getter | The underlying Map.size (includes potentially expired entries until pruned). |
Implementation Notes
size deliberately mirrors the underlying Map.size so callers can observe
hold-over expired entries before calling prune() — the counterpart has is
TTL-aware. Lookup avoids dividing by 0 (a degenerate inverse rate) by
short-circuiting the inverse branch when rate === 0; the same guard
protects triangulation against a zero fromRate. The triangulation loop
skips bases equal to from or to because those would be the inverse /
direct path the previous branches already covered.