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-virtualatpackages/finance/currency-provider-virtual/(universal-tier,0.1.0). VirtualCurrencyProvider— implementsCurrencyProviderfrom@suaveplan/currency. Constructor takes an initial rate table and an optionalClock(defaultDate.now) used forfetchRatestimestamps. No HTTP, no API key, noFetchLike.- Programmable rate-table API:
setRate(from, to, rate),removeRate(from, to): boolean,clear(), plus the read helpershasRate(from, to),getRate(from, to),listCurrencies(): readonly string[]. fetchRates(options?)— returns every rate denominated against the requestedbase(default the provider'sdefaultBase), filtered by the optionaltargetsset, withtimestampderived from the injectedClock.- Raises
CurrencyProviderError(UNKNOWN_CURRENCY)(registered in@suaveplan/currency-providers) whenfetchRatesis asked for a base that is not present in the rate table — there is no triangulation, the registry can be empty.
KEPT (inherited)
CurrencyProvidercontract from@suaveplan/currency(unchanged).CurrencyProviderError+CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCYfrom@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/runnerimports, 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/moneyregisterCurrencyintegration (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.
AbortSignalplumbing onfetchRates— the operation is synchronous over an in-memoryMap; an injectedsignalwould have nothing to abort. The optionalsignalfromRateFetchOptionsis accepted by the type but unused.