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

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 from package.json.

ADDED

  • CurrencyProviderRegistry — in-memory registry of CurrencyProvider instances keyed by name. Methods: register(provider), get(name), has(name), list(), unregister(name). get(unknown) throws CurrencyProviderError(PROVIDER_NOT_REGISTERED); register(duplicate) throws CurrencyProviderError(PROVIDER_ALREADY_REGISTERED).
  • defineCurrencyProvider(definition) — factory that returns a stable-shape CurrencyProvider. 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 injected FetchLike, applies HttpFetchOptions.timeoutMs and headers, composes the caller's AbortSignal with the timeout signal, throws CurrencyProviderError(HTTP_ERROR) on non-2xx with status populated.
  • composeAbortSignal() shared helper — merges an optional caller signal with an optional timeout-derived signal into a single AbortSignal.
  • PROVIDER_NOT_REGISTERED and PROVIDER_ALREADY_REGISTERED codes added to CURRENCY_PROVIDER_CODES.

KEPT (unchanged)

  • ApiKeyConfig, NoKeyConfig, HttpFetchOptions, ProviderConfigBase shared config types — every sibling provider extends these.
  • CURRENCY_PROVIDER_CODES registry (now 8 codes: original 6 plus the 2 registry codes) + CurrencyProviderError class with providerName / status / upstreamMessage discriminants.
  • Universal-tier purity, sideEffects: false, explicit named barrels, @suaveplan/testing/runner test 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 (the CurrencyProvider contract lives in @suaveplan/currency and 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's design.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 the CurrencyProvider contract through the registry exactly the same way.