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

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 (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

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.