5.3 KiB
registry — Name-keyed lookup of CurrencyProvider instances
(no submodules)
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
CurrencyProviderErroron every failure mode (no plainErrors) registerthrowsPROVIDER_ALREADY_REGISTEREDon duplicate names — explicitunregister+registerrequired to replacegetthrowsPROVIDER_NOT_REGISTEREDon unknown names;hasis a non-throwing probe for the same conditionlist()returns a sorted snapshot so equal sets of providers produce identical listings regardless of insertion order — keeps downstream golden-file tests deterministicunregisteris idempotent — returnstruewhen something was removed,falseotherwise, never throws- Providers stored by reference (no cloning / freezing / defensive copy)
Usage: Basic Example
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:
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 aMap<string, CurrencyProvider>internally.register(provider: CurrencyProvider): void— store the provider underprovider.name. ThrowsCurrencyProviderErrorwithcode === "CURRENCY_PROVIDER.PROVIDER_ALREADY_REGISTERED"andproviderName === provider.namewhen the name is already in use.get(name: string): CurrencyProvider— return the registered provider. ThrowsCurrencyProviderErrorwithcode === "CURRENCY_PROVIDER.PROVIDER_NOT_REGISTERED"andproviderName === namewhen nothing is registered undername.has(name: string): boolean— non-throwing probe.list(): readonly string[]— sorted snapshot of currently registered names.unregister(name: string): boolean—truewhen something was removed,falseotherwise. 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.
registeris idempotent only at the identity level: registering the same instance under the same name twice still throwsPROVIDER_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 freshstring[]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 — areadonly string[]type hint conveys the intent at the TypeScript boundary.- Both
PROVIDER_NOT_REGISTEREDandPROVIDER_ALREADY_REGISTEREDcarryproviderNameon the thrownCurrencyProviderError, so callers writing cross-cutting error handlers (audit logs, telemetry) can structure on the discriminant rather than parsing the message string.