129 lines
5.3 KiB
Markdown
129 lines
5.3 KiB
Markdown
# `registry` — Name-keyed lookup of CurrencyProvider instances
|
|
|
|
<!-- begin:auto-docs -->
|
|
_(no submodules)_
|
|
<!-- end:auto-docs -->
|
|
|
|
## Purpose
|
|
|
|
Defines `CurrencyProviderRegistry`, the in-memory registry that sibling
|
|
`@suaveplan/currency-provider-<name>` packages register their providers
|
|
with and that consumers (the `CurrencyConverter` wiring, future
|
|
`AggregatorProvider` / `RateLimitedProvider` wrappers, admin UIs that toggle
|
|
providers on and off) use to resolve providers by name without tracking
|
|
concrete classes themselves.
|
|
|
|
The registry is **opt-in convenience**, not a required indirection:
|
|
`new CurrencyConverter({ provider: new EcbProvider() })` continues to work
|
|
without a registry. The registry exists for callers that wire many providers
|
|
from configuration, that need a single canonical instance of each provider,
|
|
or that want a uniform "look up by name" surface to feed into wrappers.
|
|
|
|
## Features
|
|
|
|
- Five-method API — `register`, `get`, `has`, `list`, `unregister`
|
|
- Typed `CurrencyProviderError` on every failure mode (no plain `Error`s)
|
|
- `register` throws `PROVIDER_ALREADY_REGISTERED` on duplicate names —
|
|
explicit `unregister` + `register` required to replace
|
|
- `get` throws `PROVIDER_NOT_REGISTERED` on unknown names; `has` is a
|
|
non-throwing probe for the same condition
|
|
- `list()` returns a **sorted snapshot** so equal sets of providers
|
|
produce identical listings regardless of insertion order — keeps
|
|
downstream golden-file tests deterministic
|
|
- `unregister` is idempotent — returns `true` when something was removed,
|
|
`false` otherwise, never throws
|
|
- Providers stored by reference (no cloning / freezing / defensive copy)
|
|
|
|
## Usage: Basic Example
|
|
|
|
```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")],
|
|
});
|
|
```
|
|
|
|
## Usage: Advanced Example
|
|
|
|
Wire providers from a config file, swap them at runtime, and enumerate them
|
|
deterministically for a status dashboard:
|
|
|
|
```ts
|
|
import { CurrencyProviderRegistry } from "@suaveplan/currency-providers";
|
|
import type { CurrencyProvider } from "@suaveplan/currency";
|
|
|
|
interface ProviderModule {
|
|
readonly create: () => CurrencyProvider;
|
|
}
|
|
|
|
function wire(
|
|
modules: Readonly<Record<string, ProviderModule>>,
|
|
enabled: readonly string[]
|
|
): CurrencyProviderRegistry {
|
|
const registry = new CurrencyProviderRegistry();
|
|
for (const key of enabled) {
|
|
const mod = modules[key];
|
|
if (mod !== undefined) registry.register(mod.create());
|
|
}
|
|
return registry;
|
|
}
|
|
|
|
// Swap "ecb" for a different ECB instance with a custom fetch
|
|
const registry = wire(modules, ["ecb", "coingecko", "kraken"]);
|
|
registry.unregister("ecb");
|
|
registry.register(new EcbProvider({ fetch: instrumentedFetch }));
|
|
|
|
// Dashboard: always shows providers alphabetically
|
|
for (const name of registry.list()) {
|
|
console.log(`provider: ${name}`);
|
|
}
|
|
```
|
|
|
|
## API Reference
|
|
|
|
- `class CurrencyProviderRegistry` — exported class. Holds a `Map<string,
|
|
CurrencyProvider>` internally.
|
|
- `register(provider: CurrencyProvider): void` — store the provider under
|
|
`provider.name`. Throws `CurrencyProviderError` with
|
|
`code === "CURRENCY_PROVIDER.PROVIDER_ALREADY_REGISTERED"` and
|
|
`providerName === provider.name` when the name is already in use.
|
|
- `get(name: string): CurrencyProvider` — return the registered provider.
|
|
Throws `CurrencyProviderError` with
|
|
`code === "CURRENCY_PROVIDER.PROVIDER_NOT_REGISTERED"` and
|
|
`providerName === name` when nothing is registered under `name`.
|
|
- `has(name: string): boolean` — non-throwing probe.
|
|
- `list(): readonly string[]` — sorted snapshot of currently registered
|
|
names.
|
|
- `unregister(name: string): boolean` — `true` when something was
|
|
removed, `false` otherwise. Never throws.
|
|
|
|
## Implementation Notes
|
|
|
|
- The registry is **single-tenant**: there is no global / singleton
|
|
instance. Callers construct their own registry. Multiple registries can
|
|
coexist (e.g. one per tenant in a multi-tenant SaaS), and a provider
|
|
instance may be registered with more than one registry simultaneously.
|
|
- `register` is idempotent only at the identity level: registering the same
|
|
instance under the same name twice still throws
|
|
`PROVIDER_ALREADY_REGISTERED`, exactly as registering two different
|
|
instances under the same name throws. Idempotent "replace if newer"
|
|
semantics belong in a wrapper, not the core, so the contract here stays
|
|
unambiguous.
|
|
- `list()` returns a fresh `string[]` on every call (the sort happens
|
|
in-place on a copy). Callers may treat the result as immutable, but the
|
|
registry does not freeze it — a `readonly string[]` type hint conveys the
|
|
intent at the TypeScript boundary.
|
|
- Both `PROVIDER_NOT_REGISTERED` and `PROVIDER_ALREADY_REGISTERED` carry
|
|
`providerName` on the thrown `CurrencyProviderError`, so callers writing
|
|
cross-cutting error handlers (audit logs, telemetry) can structure on the
|
|
discriminant rather than parsing the message string.
|