10 KiB
Slim @suaveplan/currency-providers to a thin plugin core
Package: @suaveplan/currency-providers
Status: PROPOSED
Type: refactor (modification of the just-shipped 2026-05-17 migration)
- Design: ./design.md
- Spec delta: ./specs/currency-providers/spec.md
Why
The first shipped shape of @suaveplan/currency-providers bundled nine concrete
providers (ECB, Fixer, Open Exchange Rates, Binance, CoinGecko, CoinMarketCap,
CryptoCompare, Kraken, Virtual) into a single npm artefact. That bundling
violates the foundation-with-sibling-plugins shape that @suaveplan/network
and @suaveplan/drive established for genesis: every concrete plugin is its
own publishable package so consumers install only what they wire in, and each
plugin can iterate on its own version cadence without a coordinated bump of
the umbrella.
Consumers that want only ECB pay the install + tree-shake-graph cost of eight
unused providers. Worse, an upstream API change in (say) CoinMarketCap forces
a rev of the umbrella that every other provider also ships under. Splitting
into currency-providers (the contract + registry + shared HTTP helper) and
nine currency-provider-<name> siblings (in the follow-up wave) gives
consumers granular installs, gives each provider an independent surface to
evolve, and matches the established network-plugin-* / drive-plugin-*
shape.
This change is the slim: it deletes the nine concrete provider modules
from this package, adds the CurrencyProviderRegistry + defineCurrencyProvider
factory + PROVIDER_NOT_REGISTERED code that the nine sibling packages will
consume, and factors the small HTTP utilities (httpRequest,
composeAbortSignal) that every provider repeated by hand into a single
shared helper. The nine sibling packages land in the follow-up wave; each
will depend on this slimmed core and re-implement only its own provider.
What changes
REMOVED
EcbProvider— moves to@suaveplan/currency-provider-ecb.FixerProvider— moves to@suaveplan/currency-provider-fixer.OpenExchangeRatesProvider— moves to@suaveplan/currency-provider-open-exchange-rates.BinanceProvider— moves to@suaveplan/currency-provider-binance.CoinGeckoProvider+getCoinGeckoId— move to@suaveplan/currency-provider-coingecko.CoinMarketCapProvider— moves to@suaveplan/currency-provider-coinmarketcap.CryptoCompareProvider— moves to@suaveplan/currency-provider-cryptocompare.KrakenProvider— moves to@suaveplan/currency-provider-kraken.VirtualCurrencyProvider+VirtualCurrencyProviderConfig— move to@suaveplan/currency-provider-virtual.- All nine
./providers/<name>/<name>subpath exports drop frompackage.json.
ADDED
CurrencyProviderRegistry— in-memory registry ofCurrencyProviderinstances keyed byname. Methods:register(provider),get(name),has(name),list(),unregister(name).get(unknown)throwsCurrencyProviderError(PROVIDER_NOT_REGISTERED);register(duplicate)throwsCurrencyProviderError(PROVIDER_ALREADY_REGISTERED).defineCurrencyProvider(definition)— factory that returns a stable-shapeCurrencyProvider. Lets sibling packages declare a provider as a plain object literal without re-implementing the class boilerplate.httpRequest()shared helper — issues a request via the injectedFetchLike, appliesHttpFetchOptions.timeoutMsandheaders, composes the caller'sAbortSignalwith the timeout signal, throwsCurrencyProviderError(HTTP_ERROR)on non-2xx withstatuspopulated.composeAbortSignal()shared helper — merges an optional caller signal with an optional timeout-derived signal into a singleAbortSignal.PROVIDER_NOT_REGISTEREDandPROVIDER_ALREADY_REGISTEREDcodes added toCURRENCY_PROVIDER_CODES.
KEPT (unchanged)
ApiKeyConfig,NoKeyConfig,HttpFetchOptions,ProviderConfigBaseshared config types — every sibling provider extends these.CURRENCY_PROVIDER_CODESregistry (now 8 codes: original 6 plus the 2 registry codes) +CurrencyProviderErrorclass withproviderName/status/upstreamMessagediscriminants.- Universal-tier purity,
sideEffects: false, explicit named barrels,@suaveplan/testing/runnertest imports, 100/100/100/100 coverage gate.
Source mapping
This refactor is a MODIFICATION of openspec/archive/2026-05-17-add-currency-providers/.
The provider implementations now slated for removal are at:
| Removed module | Source in just-shipped archive |
|---|---|
EcbProvider |
src/providers/ecb/ecb.ts L1-L111 (per archived spec L84-L122) |
FixerProvider |
src/providers/fixer/fixer.ts (per archived spec L126-L158) |
OpenExchangeRatesProvider |
src/providers/open-exchange-rates/open-exchange-rates.ts (per archived spec L162-L187) |
BinanceProvider |
src/providers/binance/binance.ts (per archived spec L191-L217) |
CoinGeckoProvider + getCoinGeckoId |
src/providers/coingecko/coingecko.ts (per archived spec L221-L249) |
CoinMarketCapProvider |
src/providers/coinmarketcap/coinmarketcap.ts (per archived spec L253-L276) |
CryptoCompareProvider |
src/providers/cryptocompare/cryptocompare.ts (per archived spec L280-L299) |
KrakenProvider |
src/providers/kraken/kraken.ts (per archived spec L303-L325) |
VirtualCurrencyProvider |
src/providers/virtual/virtual.ts (per archived spec L329-L375) |
The new registry / factory / HTTP helper surface is genesis-original, modelled on the established foundation-with-sibling-plugins shapes:
| New module | Reference shape |
|---|---|
CurrencyProviderRegistry |
packages/network/network/src/manager/ PluginManager |
defineCurrencyProvider |
packages/drive/drive/src/plugin/ factory pattern |
httpRequest + composeAbortSignal |
extracted from the duplicated fetchImpl(url); if (!response.ok) throw HTTP_ERROR block present in every provider being removed |
The enterprise audit reference is ../suaveplan/enterprise/currency-providers.md;
the slim shape was not present upstream — it is a genesis-native architectural
correction.
Impact
Affected dependents
Run bunx turbo ls --filter=...@suaveplan/currency-providers to enumerate.
At time of writing there are no current dependents (the package shipped on
2026-05-17 and the commerce stack downstream is still pending migration):
- No current dependents (verified by
bunx turbo ls --filter=...@suaveplan/currency-providers).
The future wave of sibling provider packages will all depend on this slimmed core. Each is listed here so the next reviewer can see the blast radius the follow-up wave will land in:
@suaveplan/currency-provider-ecb— [x] consumes registry / factory / httpRequest / configs / error@suaveplan/currency-provider-fixer— [x] consumes registry / factory / httpRequest / configs / error@suaveplan/currency-provider-open-exchange-rates— [x] consumes registry / factory / httpRequest / configs / error@suaveplan/currency-provider-binance— [x] consumes registry / factory / httpRequest / configs / error@suaveplan/currency-provider-coingecko— [x] consumes registry / factory / httpRequest / configs / error@suaveplan/currency-provider-coinmarketcap— [x] consumes registry / factory / httpRequest / configs / error@suaveplan/currency-provider-cryptocompare— [x] consumes registry / factory / httpRequest / configs / error@suaveplan/currency-provider-kraken— [x] consumes registry / factory / httpRequest / configs / error@suaveplan/currency-provider-virtual— [x] consumes registry / factory / configs / error (no HTTP)@suaveplan/currency— [ ] tests run / [x] docs reviewed / [x] no action needed (theCurrencyProvidercontract lives in@suaveplan/currencyand is unchanged by this refactor; the slim core depends on it, not the other way around)
Migration
The nine concrete provider classes are gone from @suaveplan/currency-providers
and now live in dedicated sibling packages. Consumers must install the
specific provider package they need and either instantiate it directly or
register it with the new CurrencyProviderRegistry.
Before
import { CurrencyConverter } from "@suaveplan/currency";
import { EcbProvider, CoinGeckoProvider } from "@suaveplan/currency-providers";
const converter = new CurrencyConverter({
provider: new EcbProvider(),
fallbackProviders: [new CoinGeckoProvider()],
});
After
import { CurrencyConverter } from "@suaveplan/currency";
import { CurrencyProviderRegistry } from "@suaveplan/currency-providers";
import { EcbProvider } from "@suaveplan/currency-provider-ecb";
import { CoinGeckoProvider } from "@suaveplan/currency-provider-coingecko";
const registry = new CurrencyProviderRegistry();
registry.register(new EcbProvider());
registry.register(new CoinGeckoProvider());
const converter = new CurrencyConverter({
provider: registry.get("ecb"),
fallbackProviders: [registry.get("coingecko")],
});
Direct instantiation without the registry is equally supported — the registry
is a convenience for callers that wire many providers and want a single
lookup surface. Sibling packages remain CurrencyProvider-compatible so the
old new EcbProvider() form still works once the sibling package is installed.
Consumers should run this search command to find call sites that need to add sibling-package imports:
grep -rln "from ['\"]@suaveplan/currency-providers['\"]" \
packages/ apps/ src/ 2>/dev/null | grep -v "/dist/\|/coverage/"
Every hit that uses one of EcbProvider, FixerProvider,
OpenExchangeRatesProvider, BinanceProvider, CoinGeckoProvider,
CoinMarketCapProvider, CryptoCompareProvider, KrakenProvider,
VirtualCurrencyProvider, VirtualCurrencyProviderConfig, or
getCoinGeckoId needs its import rewritten to the matching
@suaveplan/currency-provider-<name> package.
Out of scope
- The nine sibling
@suaveplan/currency-provider-<name>packages — each ships under its own OpenSpec change in the follow-up wave per AGENTS.md §0c "Multi-package / cross-cutting feature sets" (one change per package; cross-cutting design lives once, in this package'sdesign.md). - Rate-limit / retry / historical-rate / WebSocket / on-chain oracle /
aggregator / health-monitor / OTel surfaces — same deferral as the
2026-05-17 archive's
## Out of scope. Nothing in this slim refactor changes those plans; once those wrappers land they consume theCurrencyProvidercontract through the registry exactly the same way.