Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# @suaveplan/currency-provider-virtual - Universal in-memory virtual / game currency provider for @suaveplan/currency. Programmable rate table (setRate / removeRate / clear) with optional Clock injection for deterministic timestamps. No HTTP, no API key, no FetchLike. Throws typed CurrencyProviderError(UNKNOWN_CURRENCY) when the requested base is absent. Sibling of the @suaveplan/currency-providers slim core.
|
||||
|
||||
In-memory, programmable virtual / game currency provider for
|
||||
`@suaveplan/currency`. Sibling of the `@suaveplan/currency-providers`
|
||||
slim plugin core. No HTTP, no API key, no `FetchLike` — purely an
|
||||
operator-controlled rate table backed by a nested `Map`.
|
||||
|
||||
Use it for game economies, loyalty points, internal test rates, or any
|
||||
rate source you want to seed and mutate deterministically in process.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module surfaces the single `CURRENCY_PROVIDER.UNKNOWN_CURRENCY` code that `@suaveplan/currency-provider-virtual` r…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module surfaces the single `CURRENCY_PROVIDER.UNKNOWN_CURRENCY` code that `@suaveplan/currency-provider-virtual` r…
|
||||
- [provider](./src/provider/provider.md) — `VirtualCurrencyProvider` is an in-memory, programmable rate-source implementation of `@suaveplan/currency`'s `Currency…
|
||||
- [provider](./src/provider/provider.md) — `VirtualCurrencyProvider` is an in-memory, programmable rate-source implementation of `@suaveplan/currency`'s `Currency…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/currency-provider-virtual @suaveplan/currency-providers @suaveplan/currency
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { CurrencyConverter } from "@suaveplan/currency";
|
||||
import { CurrencyProviderRegistry } from "@suaveplan/currency-providers";
|
||||
import { VirtualCurrencyProvider } from "@suaveplan/currency-provider-virtual";
|
||||
|
||||
const virtual = new VirtualCurrencyProvider({
|
||||
defaultBase: "USD",
|
||||
initialRates: [
|
||||
{ from: "USD", to: "GOLD", rate: 100 },
|
||||
{ from: "USD", to: "GEMS", rate: 500 },
|
||||
],
|
||||
});
|
||||
|
||||
const registry = new CurrencyProviderRegistry();
|
||||
registry.register(virtual);
|
||||
|
||||
const converter = new CurrencyConverter({ provider: registry.get("virtual-currency") });
|
||||
const rate = await converter.getRate("USD", "GOLD"); // 100
|
||||
|
||||
// Operators can mutate at any time.
|
||||
virtual.setRate("USD", "GOLD", 110);
|
||||
virtual.removeRate("USD", "GEMS");
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Programmable rate table: `setRate(from, to, rate)` (upsert),
|
||||
`removeRate(from, to)` (idempotent), `clear()`,
|
||||
`hasRate(from, to)`, `getRate(from, to)`,
|
||||
`listCurrencies(): readonly string[]` (sorted snapshot).
|
||||
- `fetchRates(options?)` returns directed rates stored under the
|
||||
requested `base`, optionally filtered by `options.targets`.
|
||||
- Typed `CurrencyProviderError(UNKNOWN_CURRENCY)` raised when the
|
||||
requested base has no rates stored (no triangulation; the table can
|
||||
be empty).
|
||||
- Optional `Clock` injection (`@suaveplan/types`) for deterministic
|
||||
`RateFetchResult.timestamp` values under `FakeClock`-driven tests.
|
||||
- Implements the `CurrencyProvider` contract from `@suaveplan/currency`
|
||||
with `supportsVirtual: true`, `supportsFiat: false`,
|
||||
`supportsCrypto: false`, `name: "virtual-currency"`.
|
||||
- Universal — runs unchanged on Node 18+, Bun, browsers, and Web
|
||||
Workers. Zero browser/Node/DOM APIs in production source.
|
||||
- `sideEffects: false` — the constructor never mutates global state
|
||||
(no `@suaveplan/money` `registerCurrency` integration).
|
||||
- Explicit named barrels; two subpath exports; 100/100/100/100
|
||||
istanbul coverage.
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/error-codes/error-codes.md`](./src/error-codes/error-codes.md) —
|
||||
`VIRTUAL_CURRENCY_PROVIDER_CODES`, `VIRTUAL_PROVIDER_NAME`, and the
|
||||
`makeUnknownCurrencyError(base)` helper.
|
||||
- [`src/provider/provider.md`](./src/provider/provider.md) —
|
||||
`VirtualCurrencyProvider` class + `VirtualCurrencyProviderConfig` and
|
||||
`VirtualRateEntry` types.
|
||||
|
||||
## Status
|
||||
|
||||
`0.1.0` (pre-graduation). Sibling of the
|
||||
`@suaveplan/currency-providers` slim core landed on 2026-05-17. See
|
||||
the design proposal
|
||||
for the migration record. The cross-cutting architecture for the
|
||||
`@suaveplan/currency-provider-*` family is documented in
|
||||
the design document.
|
||||
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user