# `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 (`set` records `Date.now() + ttlMs` as `expiresAt`). - 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 (or `undefined` for 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 ```ts 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: ```ts 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.