first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
# error-codes — typed error helpers for the virtual provider
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module surfaces the single `CURRENCY_PROVIDER.UNKNOWN_CURRENCY` code
that `@suaveplan/currency-provider-virtual` raises, plus a tiny
`makeUnknownCurrencyError(base)` builder that constructs the typed
`CurrencyProviderError` with the canonical message format and
`providerName: "virtual-currency"` discriminant pre-populated. The full
8-code registry and the `CurrencyProviderError` class itself live in the
`@suaveplan/currency-providers` slim core — this module never adds new
codes, it only re-narrows the surface the virtual provider actually uses.
The `VIRTUAL_PROVIDER_NAME` constant is the stable identifier exposed as
`provider.name` and stamped onto every emitted error's `providerName`
discriminant, so consumer assertions can pin to a single source of truth.
## Features
- `VIRTUAL_CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCY` — re-export of the
slim-core `CURRENCY_PROVIDER.UNKNOWN_CURRENCY` code (string-identical).
- `VIRTUAL_PROVIDER_NAME` — `"virtual-currency"`.
- `makeUnknownCurrencyError(base)` — returns a `CurrencyProviderError`
with the canonical message and `providerName` discriminant set.
## Usage: Basic
```ts
import {
makeUnknownCurrencyError,
VIRTUAL_CURRENCY_PROVIDER_CODES,
} from "@suaveplan/currency-provider-virtual/error-codes/error-codes";
const error = makeUnknownCurrencyError("NOPE");
console.log(error.code); // "CURRENCY_PROVIDER.UNKNOWN_CURRENCY"
console.log(error.providerName); // "virtual-currency"
```
## Usage: Advanced
```ts
import { CurrencyProviderError } from "@suaveplan/currency-providers";
import {
makeUnknownCurrencyError,
VIRTUAL_CURRENCY_PROVIDER_CODES,
} from "@suaveplan/currency-provider-virtual/error-codes/error-codes";
function isUnknownVirtualCurrency(err: unknown): err is CurrencyProviderError {
return (
err instanceof CurrencyProviderError &&
err.code === VIRTUAL_CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCY &&
err.providerName === "virtual-currency"
);
}
try {
throw makeUnknownCurrencyError("GHOST");
} catch (err) {
if (isUnknownVirtualCurrency(err)) {
// Fall back to the next provider in the chain.
} else {
throw err;
}
}
```
## API Reference
### `VIRTUAL_CURRENCY_PROVIDER_CODES`
Frozen record. Contains exactly one key — `UNKNOWN_CURRENCY` — whose
value is the namespaced string `"CURRENCY_PROVIDER.UNKNOWN_CURRENCY"`
imported from the slim core. Reference-equal with the slim-core
constant; comparisons via `===` work as expected.
### `VIRTUAL_PROVIDER_NAME`
`"virtual-currency"`. Stable identifier used as `provider.name` and as
the `providerName` discriminant on every emitted error.
### `makeUnknownCurrencyError(base: string): CurrencyProviderError`
Builds the canonical error for a missing-base lookup.
- `base` — the unknown base currency code; included verbatim in the
message.
- Returns a `CurrencyProviderError` with
`code === "CURRENCY_PROVIDER.UNKNOWN_CURRENCY"`,
`providerName === "virtual-currency"`, and message format
`"VirtualCurrencyProvider: no rates stored for base \"<base>\""`.
## Implementation Notes
- This module owns zero codes — it re-exports one from the slim core.
Adding a new code requires an OpenSpec change against
`@suaveplan/currency-providers` (which holds the canonical registry),
not against this package.
- The helper is `const`-only — no top-level state, no I/O, no closures
captured — so it remains tree-shake-friendly under
`"sideEffects": false`.

View File

@@ -0,0 +1,160 @@
# provider — VirtualCurrencyProvider
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`VirtualCurrencyProvider` is an in-memory, programmable rate-source
implementation of `@suaveplan/currency`'s `CurrencyProvider` contract.
It serves operator-defined virtual / game currencies (and any internal
test rates) without ever issuing an HTTP request or consuming a
`FetchLike`. Rates are stored as a nested `Map<from, Map<to, number>>`
and mutated at runtime through `setRate`, `removeRate`, and `clear`.
The provider sets `supportsVirtual: true` and is the only sibling in
the `@suaveplan/currency-provider-*` family that does not depend on the
slim core's `httpRequest` helper. Use it for game economies, loyalty
points, internal test fixtures, or any rate table you want to control
deterministically.
## Features
- `setRate(from, to, rate)` — upsert a directed rate; replaces an
existing pair without throwing.
- `removeRate(from, to): boolean` — idempotent removal; returns whether
anything was actually removed. Auto-cleans the inner map when the
last rate for a `from` is dropped.
- `clear()` — empty the table in one call.
- `hasRate(from, to): boolean`, `getRate(from, to): number | undefined`,
`listCurrencies(): readonly string[]` — read-only helpers.
- `fetchRates(options?)` — returns the rates stored under
`options.base` (default `defaultBase`), optionally filtered by an
`options.targets` inclusion list. Throws
`CurrencyProviderError(CURRENCY_PROVIDER.UNKNOWN_CURRENCY)` when the
requested base is not in the table.
- Optional `Clock` injection for deterministic `timestamp` values.
- Universal — zero browser / Node / DOM APIs, `sideEffects: false`.
## Usage: Basic
```ts
import { VirtualCurrencyProvider } from "@suaveplan/currency-provider-virtual";
const provider = new VirtualCurrencyProvider({
defaultBase: "USD",
initialRates: [
{ from: "USD", to: "GOLD", rate: 100 },
{ from: "USD", to: "GEMS", rate: 500 },
],
});
const result = await provider.fetchRates();
// { base: "USD", rates: { GOLD: 100, GEMS: 500 }, timestamp: <Date.now()> }
```
## Usage: Advanced
```ts
import { FakeClock } from "@suaveplan/testing/clock/fake-clock";
import { VirtualCurrencyProvider } from "@suaveplan/currency-provider-virtual";
import { CURRENCY_PROVIDER_CODES, CurrencyProviderError } from "@suaveplan/currency-providers";
// Deterministic timestamps + partial-graph rates (no triangulation).
const fakeClock = new FakeClock({ startTime: 1_700_000_000_000 });
const provider = new VirtualCurrencyProvider({
defaultBase: "GOLD",
clock: () => fakeClock.now(),
});
provider.setRate("GOLD", "GEMS", 5);
provider.setRate("GOLD", "USD", 0.01);
const golden = await provider.fetchRates({ base: "GOLD", targets: ["GEMS"] });
// { base: "GOLD", rates: { GEMS: 5 }, timestamp: 1_700_000_000_000 }
fakeClock.advance(1000);
const stale = await provider.fetchRates({ base: "GOLD" });
// { base: "GOLD", rates: { GEMS: 5, USD: 0.01 }, timestamp: 1_700_000_001_000 }
// Operator updates a rate at runtime — next fetchRates picks it up.
provider.setRate("GOLD", "GEMS", 6);
// Unknown base raises the typed error.
try {
await provider.fetchRates({ base: "GHOST" });
} catch (err) {
if (
err instanceof CurrencyProviderError &&
err.code === CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCY
) {
// expected
}
}
```
## API Reference
### `class VirtualCurrencyProvider implements CurrencyProvider`
Constructor: `new VirtualCurrencyProvider(config?: VirtualCurrencyProviderConfig)`.
Static identity:
- `name === "virtual-currency"`
- `supportsCrypto === false`
- `supportsFiat === false`
- `supportsVirtual === true`
Methods:
- `setRate(from: string, to: string, rate: number): void` — upsert. No
throw on duplicate pair.
- `removeRate(from: string, to: string): boolean` — idempotent. Never
throws.
- `clear(): void` — empties the table.
- `hasRate(from: string, to: string): boolean`
- `getRate(from: string, to: string): number | undefined`
- `listCurrencies(): readonly string[]` — sorted snapshot of every
`from` key.
- `fetchRates(options?: RateFetchOptions): Promise<RateFetchResult>` —
rejects with `CurrencyProviderError(UNKNOWN_CURRENCY)` when the
requested base is not stored; otherwise resolves to the rate map for
that base, filtered by `options.targets` when provided, with
`timestamp = clock()`.
### `interface VirtualCurrencyProviderConfig`
- `defaultBase?: string` — base used when `fetchRates()` omits
`options.base`. Defaults to `"USD"`.
- `initialRates?: readonly VirtualRateEntry[]` — entries upserted at
construction. Duplicates within the array are last-write-wins.
- `clock?: Clock` — `() => number` (ms since epoch). Defaults to
`() => Date.now()`.
### `interface VirtualRateEntry`
`{ from: string; to: string; rate: number }`.
## Implementation Notes
- The rate table is `Map<from, Map<to, number>>` — O(1) lookup,
O(n) enumeration over the inner map. The flat alternative
(`Map<"${from}->${to}", number>`) was rejected because currency codes
can contain `-` (e.g. `USDC-GOLD`, `GAME-1-COIN`) which would create
ambiguous keys.
- `removeRate` deletes the empty inner map after the last pair under a
`from` is removed, so `listCurrencies()` stays accurate without a
separate compaction pass.
- The `fetchRates` "no I/O" promise wrapping satisfies the
`CurrencyProvider` async contract without introducing a microtask
delay observable to callers; the work is purely synchronous.
- `Clock` is invoked once per `fetchRates` call, at the moment of the
call (not construction). This matches the wall-clock semantics
every other provider in the family uses.
- The provider is intentionally side-effect-free in its constructor
(unlike the upstream form which called `@suaveplan/money`
`registerCurrency`) — keeping `"sideEffects": false` honest and
tree-shake safe.