Files
suaveplan/openspec/archive/2026-05-17-add-currency-provider-virtual/proposal.md
2026-05-27 23:29:53 +01:00

5.9 KiB

Add @suaveplan/currency-provider-virtual

Package: @suaveplan/currency-provider-virtual Status: PROPOSED Type: new package (sibling of @suaveplan/currency-providers slim core)

  • Design: ./design.md
  • Spec delta: ./specs/currency-provider-virtual/spec.md

Why

The 2026-05-17 slim-currency-providers refactor split the original 9-provider bundle into a slim core (@suaveplan/currency-providers) plus nine sibling @suaveplan/currency-provider-<name> packages. Of the nine, eight wrap remote HTTP APIs (ECB, Fixer, OXR, Binance, CoinGecko, CoinMarketCap, CryptoCompare, Kraken); the ninth — virtual — serves operator-defined virtual / game currencies whose rates are programmable in memory and never round-trip a network.

This change lands the virtual sibling. It is the only sibling that does NOT consume httpRequest / FetchLike from the slim core (it has no upstream to fetch) and that DOES set supportsVirtual: true on the CurrencyProvider contract.

What changes

ADDED

  • New package @suaveplan/currency-provider-virtual at packages/finance/currency-provider-virtual/ (universal-tier, 0.1.0).
  • VirtualCurrencyProvider — implements CurrencyProvider from @suaveplan/currency. Constructor takes an initial rate table and an optional Clock (default Date.now) used for fetchRates timestamps. No HTTP, no API key, no FetchLike.
  • Programmable rate-table API: setRate(from, to, rate), removeRate(from, to): boolean, clear(), plus the read helpers hasRate(from, to), getRate(from, to), listCurrencies(): readonly string[].
  • fetchRates(options?) — returns every rate denominated against the requested base (default the provider's defaultBase), filtered by the optional targets set, with timestamp derived from the injected Clock.
  • Raises CurrencyProviderError(UNKNOWN_CURRENCY) (registered in @suaveplan/currency-providers) when fetchRates is asked for a base that is not present in the rate table — there is no triangulation, the registry can be empty.

KEPT (inherited)

  • CurrencyProvider contract from @suaveplan/currency (unchanged).
  • CurrencyProviderError + CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCY from @suaveplan/currency-providers (already present in the 8-code slim-core registry).
  • Universal-tier purity (zero browser / Node APIs in src/), explicit named barrels, @suaveplan/testing/runner imports, 100/100/100/100 istanbul coverage.

Source mapping

The upstream reference shape lived in ../suaveplan/packages/finance/currency-providers/src/virtual/virtual.ts (pre-slim, now removed). That upstream shape coupled the provider to @suaveplan/money (registerCurrency) and stored rates as a record of VirtualCurrencyConfig { rate, name, symbol, exponent?, issuerId? } keyed by currency code, assuming a single shared base.

Capability Upstream Migrated Notes
Programmable per-currency rate table ✅ ✅ Surface generalised — setRate(from, to, rate) lets callers store arbitrary directed pairs (game internal rates, partial graphs) rather than only "one base, many targets".
fetchRates({ base, targets }) ✅ ✅
supportsVirtual: true flag ✅ ✅
Triangulation from any virtual base ✅ 🚫 Scoped out: the slim-core / CurrencyConverter already handles triangulation via the rate cache; duplicating it here would split the algorithm across two packages. Callers that need triangulated rates either pre-populate both directions with setRate or wire the provider through CurrencyConverter which triangulates from the cached RateFetchResult.
Auto-registering currencies via @suaveplan/money registerCurrency ✅ 🚫 Scoped out: the slim refactor explicitly dropped the @suaveplan/money dep so the slim core stays universal and zero-side-effect. Callers that want money-registry integration call registerCurrency themselves before constructing the provider — keeps this package side-effect-free per sideEffects: false.
Clock-injected timestamp for deterministic tests ⬆️ ⬆️ Enhanced: upstream hard-coded Date.now(). We inject a Clock so FakeClock-driven tests get deterministic timestamps without monkey-patching Date.
UNKNOWN_CURRENCY typed error ⬆️ ⬆️ Enhanced: upstream threw plain Error("VirtualCurrencyProvider: no rates for base UNKNOWN"). We throw CurrencyProviderError(CURRENCY_PROVIDER.UNKNOWN_CURRENCY) with providerName populated.
defineCurrencyProvider-friendly ⬆️ ⬆️ Enhanced: the provider exposes a stable class with the programmable methods AND can be wrapped via defineCurrencyProvider if a consumer prefers the object-literal idiom.

The enterprise audit reference is ../suaveplan/enterprise/currency-providers.md; the virtual provider is one row in that audit and carried no specific gaps beyond the registry-typed-error gap, which is closed here.

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/currency-provider-virtual to enumerate.

  • No current dependents (verified by bunx turbo ls --filter=...@suaveplan/currency-provider-virtual).

The package is a leaf sibling — the future commerce / gaming stack downstream will consume it directly, but those packages are still pending migration.

Out of scope

  • Triangulation across the rate table (see Parity Checklist row).
  • @suaveplan/money registerCurrency integration (see Parity Checklist row).
  • Persistence — the rate table is purely in-memory by design (this provider's whole purpose). Callers that need persistence layer a storage adapter on top and reload the table at construction.
  • AbortSignal plumbing on fetchRates — the operation is synchronous over an in-memory Map; an injected signal would have nothing to abort. The optional signal from RateFetchOptions is accepted by the type but unused.