Initial Commit
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# Design: @suaveplan/currency-provider-virtual
|
||||
|
||||
> Non-normative. The normative requirements live in
|
||||
> `./specs/currency-provider-virtual/spec.md`. The cross-cutting
|
||||
> foundation-with-sibling-plugins architecture is documented once in
|
||||
> `openspec/archive/2026-05-17-slim-currency-providers/design.md` — this
|
||||
> document covers only what is specific to the virtual sibling.
|
||||
|
||||
## Shape
|
||||
|
||||
```text
|
||||
@suaveplan/currency-provider-virtual
|
||||
└── src/
|
||||
├── index.ts # explicit named barrel
|
||||
├── error-codes/
|
||||
│ ├── error-codes.ts # re-exports UNKNOWN_CURRENCY helper
|
||||
│ └── error-codes.md
|
||||
└── provider/
|
||||
├── provider.ts # VirtualCurrencyProvider class
|
||||
├── provider.types.ts # rate-table & options types
|
||||
├── provider.test.ts # all behaviour incl. UNKNOWN_CURRENCY
|
||||
└── provider.md
|
||||
```
|
||||
|
||||
## Rate-table storage
|
||||
|
||||
Rates are stored as a nested `Map<from, Map<to, number>>`. Pair lookup is
|
||||
O(1); enumeration for `fetchRates` is O(n) over the inner map of the
|
||||
requested base. Storing the table as nested `Map`s rather than a flat
|
||||
`Map<"${from}->${to}", number>` keeps `listCurrencies` cheap (just the outer
|
||||
key set) and avoids string-concatenation collisions when currency codes
|
||||
contain hyphens (`USDC-GOLD`, `GAME-1-COIN`).
|
||||
|
||||
Insertion replaces an existing rate silently — `setRate` is upsert, mirroring
|
||||
the upstream behaviour. This is intentional: virtual currencies are
|
||||
operator-controlled and "the operator changed the rate" is the normal flow,
|
||||
not an error.
|
||||
|
||||
## Triangulation is deliberately absent
|
||||
|
||||
The upstream provider derived `rates[base]` for non-default bases by inverting
|
||||
the stored rate (`1 / baseVc.rate`) and computing cross-rates
|
||||
(`otherRate / baseVc.rate`). Genesis pushes that into `CurrencyConverter`:
|
||||
the provider returns the rates it has, the converter triangulates via the
|
||||
cache. This keeps two responsibilities separate and avoids the upstream's
|
||||
implicit assumption that every virtual currency converts through a single
|
||||
shared `EUR` base. Callers who want bidirectional rates pre-populate them
|
||||
explicitly — usually trivial in a game-economy seeder.
|
||||
|
||||
## Clock injection
|
||||
|
||||
`Clock = () => number` from `@suaveplan/types` (ms since epoch). The
|
||||
constructor accepts an optional `clock` (default `() => Date.now()`); every
|
||||
`fetchRates` result stamps `timestamp = clock()`. Tests pass a `FakeClock`
|
||||
adapter (`() => fakeClock.now()`) for deterministic timestamps without
|
||||
monkey-patching `Date`.
|
||||
|
||||
## Error handling
|
||||
|
||||
`fetchRates({ base })` consults the rate table for `base`. If `base` is not
|
||||
present, the provider throws
|
||||
`CurrencyProviderError(CURRENCY_PROVIDER.UNKNOWN_CURRENCY)` with
|
||||
`providerName: "virtual-currency"`. This is the only error path: `setRate`
|
||||
upserts, `removeRate` is idempotent (returns `false` for missing pairs),
|
||||
`clear` always succeeds. `UNKNOWN_CURRENCY` was added to the slim-core
|
||||
registry on the original 2026-05-17 land and is the canonical typed error
|
||||
for "asked about a currency the provider doesn't know."
|
||||
|
||||
## Universal-tier purity
|
||||
|
||||
- No `node:` / `bun:` / DOM imports anywhere in `src/`.
|
||||
- No `fetch`, no network, no `FetchLike` consumption — this is the entire
|
||||
point of the package.
|
||||
- `sideEffects: false` — constructing the provider never mutates global
|
||||
state. (Contrast with the upstream form which called
|
||||
`@suaveplan/money` `registerCurrency` from the constructor, a hidden side
|
||||
effect that broke tree-shaking.)
|
||||
Reference in New Issue
Block a user