Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user