Files
skills/docs/money/finance/currency/src/rate-cache/rate-cache.md
2026-05-29 08:20:56 +01:00

92 lines
3.9 KiB
Markdown

# `rate-cache` — TTL-bounded rate cache with triangulated lookup
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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.