Initial Commit
This commit is contained in:
206
openspec/archive/2026-05-17-slim-currency-providers/proposal.md
Normal file
206
openspec/archive/2026-05-17-slim-currency-providers/proposal.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# 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
|
||||
|
||||
```ts
|
||||
import { CurrencyConverter } from "@suaveplan/currency";
|
||||
import { EcbProvider, CoinGeckoProvider } from "@suaveplan/currency-providers";
|
||||
|
||||
const converter = new CurrencyConverter({
|
||||
provider: new EcbProvider(),
|
||||
fallbackProviders: [new CoinGeckoProvider()],
|
||||
});
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```bash
|
||||
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.
|
||||
Reference in New Issue
Block a user