first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
# @suaveplan/currency-provider-ecb - Universal @suaveplan/currency provider for the European Central Bank (ECB) daily exchange-rates XML feed. Free, no API key. Implements the slim @suaveplan/currency-providers contract: typed CurrencyProviderError, injectable FetchLike from @suaveplan/types, shared httpRequest helper, mathematical conversion to any non-EUR base, AbortSignal propagation, target filtering, PARSE_ERROR / UNSUPPORTED_BASE / UNKNOWN_CURRENCY / HTTP_ERROR coverage.
Universal `@suaveplan/currency` rate provider for the European Central
Bank's daily exchange-rates XML feed. Free, no API key required. Built on the
slim `@suaveplan/currency-providers` plugin core: typed
`CurrencyProviderError` on every failure path, shared `httpRequest()` helper
for HTTP wrapping + abort-signal composition, injectable `FetchLike` from
`@suaveplan/types`, mathematical conversion to any non-EUR base, target
filtering with `UNKNOWN_CURRENCY` guarding, and `PARSE_ERROR` protection
against silently caching an empty rate map.
<!-- begin:auto-docs -->
## Documentation tree
- [ecb](./src/ecb/ecb.md) — `EcbProvider` is the `@suaveplan/currency` rate-source implementation for the European Central Bank's daily reference-r…
- [ecb](./src/ecb/ecb.md) — `EcbProvider` is the `@suaveplan/currency` rate-source implementation for the European Central Bank's daily reference-r…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/currency-provider-ecb @suaveplan/currency @suaveplan/currency-providers
```
## Quick start
```ts
import { CurrencyConverter } from "@suaveplan/currency";
import { EcbProvider } from "@suaveplan/currency-provider-ecb";
const converter = new CurrencyConverter({ provider: new EcbProvider() });
const rate = await converter.getRate("EUR", "USD");
```
Or via the slim core's registry, alongside other sibling provider packages:
```ts
import { CurrencyConverter } from "@suaveplan/currency";
import { CurrencyProviderRegistry } from "@suaveplan/currency-providers";
import { EcbProvider } from "@suaveplan/currency-provider-ecb";
const registry = new CurrencyProviderRegistry();
registry.register(new EcbProvider());
const converter = new CurrencyConverter({ provider: registry.get("ecb") });
```
## Features
- Free, no API key.
- ~30 fiat currencies, EUR-quoted upstream; non-EUR bases derived
arithmetically.
- Daily refresh (~16:00 CET); weekends / holidays return the last published
snapshot.
- `RateFetchOptions.signal` forwarded through the slim core's HTTP helper.
- Typed `CurrencyProviderError` codes: `HTTP_ERROR`, `PARSE_ERROR`,
`UNSUPPORTED_BASE`, `UNKNOWN_CURRENCY`.
- Universal (browser / Web Worker / Node 18+ / Bun).
- Zero runtime deps beyond `@suaveplan/currency`,
`@suaveplan/currency-providers`, `@suaveplan/types`, `tslib`.
## Modules
- [`src/ecb/ecb.md`](./src/ecb/ecb.md) — `EcbProvider` class, configuration,
error matrix, advanced examples.

View File

@@ -0,0 +1,143 @@
# EcbProvider
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`EcbProvider` is the `@suaveplan/currency` rate-source implementation for the
European Central Bank's daily reference-rates XML feed. It is the lowest-
friction fiat provider in the Suaveplan catalogue: free, no API key required,
universal (runs in browsers, Web Workers, Node.js, and Bun), and built on
the slim `@suaveplan/currency-providers` plugin core so every error path,
abort-signal composition, and HTTP wrapping is shared with every other sibling
provider in the family.
The package exposes a single class, `EcbProvider`, with stable identity flags
(`name === "ecb"`, `supportsFiat === true`, `supportsCrypto === false`). It is
not auto-registered with any registry — consumers wire it through either
`new CurrencyConverter({ provider: new EcbProvider() })` or
`registry.register(new EcbProvider())` against a
`CurrencyProviderRegistry` from `@suaveplan/currency-providers`.
## Features
- Free, no authentication — public ECB daily XML feed.
- ~30 fiat currencies (every major plus many minor European / global pairs).
- Updates around 16:00 CET each business day; weekends and bank holidays
return the last published snapshot.
- Default base is EUR; any non-EUR base is synthesised by inverting through
the EUR rates so callers never have to triangulate manually.
- Optional `targets` filter so consumers iterating a small basket avoid
caching the full 30-currency table.
- Typed `CurrencyProviderError` on every failure path: `HTTP_ERROR` for non-
2xx upstream responses, `PARSE_ERROR` for an unparseable body,
`UNSUPPORTED_BASE` when the requested base is missing from the EUR-quoted
table, `UNKNOWN_CURRENCY` when every requested target is absent.
- `AbortSignal` propagation — `RateFetchOptions.signal` is forwarded through
the slim core's `httpRequest()` so callers can cancel in-flight requests.
- Injectable `FetchLike` (from `@suaveplan/types`) — tests pass a recording
closure; production accepts the default `globalThis.fetch`.
- Zero runtime dependencies beyond `@suaveplan/currency-providers` /
`@suaveplan/currency` / `@suaveplan/types` / `tslib`.
## Usage
### Basic Example
```ts
import { CurrencyConverter } from "@suaveplan/currency";
import { EcbProvider } from "@suaveplan/currency-provider-ecb";
const converter = new CurrencyConverter({
provider: new EcbProvider(),
});
const rate = await converter.getRate("EUR", "USD");
console.log(`1 EUR = ${rate} USD`);
```
### Advanced Example
Wiring through the slim core's registry with a custom `baseUrl` (e.g. an
internal proxy that caches the upstream XML at the edge) and a caller-side
`AbortSignal` so a UI cancellation aborts the in-flight HTTP request.
```ts
import { CurrencyConverter } from "@suaveplan/currency";
import { CurrencyProviderRegistry } from "@suaveplan/currency-providers";
import { EcbProvider } from "@suaveplan/currency-provider-ecb";
const registry = new CurrencyProviderRegistry();
registry.register(
new EcbProvider({
baseUrl: "https://cdn.internal.example.com/ecb/daily.xml",
}),
);
const controller = new AbortController();
setTimeout(() => controller.abort(new Error("user navigated away")), 5_000);
const provider = registry.get("ecb");
const result = await provider.fetchRates({
base: "USD",
targets: ["EUR", "GBP", "JPY"],
signal: controller.signal,
});
console.log(result.base, result.rates, result.timestamp);
```
## API Reference
### `class EcbProvider implements CurrencyProvider`
#### `constructor(config?: NoKeyConfig)`
- `config.baseUrl?: string` — override the default ECB endpoint
(`https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml`).
- `config.fetch?: FetchLike` — injectable `fetch` implementation; defaults to
`globalThis.fetch` resolved at call time.
#### Read-only fields
- `name: "ecb"` — stable identifier surfaced as `CurrencyProvider.name`.
- `supportsFiat: true`
- `supportsCrypto: false`
#### `fetchRates(options?: RateFetchOptions): Promise<RateFetchResult>`
Fetches and returns the latest ECB rates. `options.base` defaults to `"EUR"`;
when set to any other currency the result is synthesised by inverting through
the EUR-quoted feed. `options.targets` filters the returned map.
`options.signal` is forwarded to the underlying HTTP call.
**Throws** `CurrencyProviderError` with one of:
- `HTTP_ERROR` — non-2xx response (raised by the slim core's `httpRequest`).
- `PARSE_ERROR` — response body contained no parseable currency tags.
- `UNSUPPORTED_BASE` — caller asked for a non-EUR base that is not in the
EUR-quoted feed.
- `UNKNOWN_CURRENCY` — every entry in `options.targets` was absent from the
resolved rate map.
## Implementation Notes
- The ECB endpoint always publishes EUR-based rates. Non-EUR bases are
derived purely arithmetically — there is no second HTTP call. This is
the same algorithm the upstream `@suaveplan/currency-providers` shipped
before the 2026-05-17 slim refactor split it into siblings.
- XML parsing uses a regex (`currency='([A-Z]+)'\s+rate='([\d.]+)'`) rather
than a full XML parser to keep the package zero-runtime-dep beyond the
slim core and `@suaveplan/currency`. The ECB feed shape is stable; if the
upstream format changes, the failure surfaces immediately as
`PARSE_ERROR`.
- Rates are not available on weekends or ECB bank holidays; the feed returns
the last published snapshot. Consumers that need same-day freshness should
layer `@suaveplan/currency`'s `staleDurationMs` on top.
- `globalThis.fetch` is resolved at call time, not constructor time, so test
setups that replace the global between construction and the first
`fetchRates()` call still work.
- The package declares `"sideEffects": false` and ships explicit named barrels
per AGENTS.md §6a; tree-shaking unused exports is safe.