first commit
This commit is contained in:
91
docs/money/finance/currency/src/rate-cache/rate-cache.md
Normal file
91
docs/money/finance/currency/src/rate-cache/rate-cache.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# `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.
|
||||
Reference in New Issue
Block a user