92 lines
3.9 KiB
Markdown
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.
|