Files
skills/docs/money/finance/currency-providers/src/registry/registry.md
2026-05-29 08:20:56 +01:00

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 CurrencyProviderError on every failure mode (no plain Errors)
  • 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

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 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.