|
|
|
|
import { CURRENCY_DIAGNOSTIC_EVENTS, RATE_PAIR_SEPARATOR } from './consts';
|
|
|
|
|
import { createCurrencyDiagnostics, emitCurrencyDiagnostic } from './diagnostics';
|
Build runtime infrastructure layer: aapp + 5 new artifacts
Adds the application composition root and six new runtime artifacts. lang
and logr alone could not cover an app surface — locale propagation, format
helpers, frontend preferences, validation, persistence and DOM service all
needed independent artifacts that aapp wires together under a single
locale source of truth and a shared logger.
New artifacts:
- aapp: composition root. createActiveApp + createTestApp +
App.createSiumEngine + frontend.persist + Storage wiring.
- stor: pluggable adapters (local/session/memory/cookie + SSR via
cookieAdapter.fromCookies), envelope versioning + migrate +
TTL, validate via Standard Schema, intra-tab + cross-tab sync,
Storage.clear(), per-entry adapter/namespace overrides.
- adom: reactive DOM service. shareViewport opt-in + per-window
tracker, dispose(). breakpoints, viewport, attribute writes.
- fend: frontend preferences (theme, mode, dir, density,
reducedMotion, reducedSound) with auto/clear/isAuto pattern.
- fmts: localized formatting (numbers, currency, units, dates) with
shared LocaleSource and per-domain auto/clear pattern.
- sium: validation engine. Standard Schema interop, Lang/Logger
injection, codec/refine/transform, domain types
(color/date/time), introspection.
Existing artifacts extended:
- lang: BCP 47 SupportedLocale (LangBase | base-region), mono lang
for monolingual apps, extend() leaf-overwrite warning,
EngineLang/ActiveLang naming alignment with the rest of the
project (no Instance suffix).
- logr: EngineLogger.dispose(), failureThrottleMs anti-cascade for
transports, README sync to the per-level filter API
(minLevel removed).
Cross-cutting:
- $locale: shared LocaleSource type consumed by fmts and fend.
- arts/README.md: artifact map, dependency graph, naming conventions.
- Test pages for every artifact under /test/<artifact>.
733 tests passing; svelte-check clean except 2 pre-existing errors on
sium/svelte provider (port pending).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
|
|
|
import { assertValidRate, isValidRate, normalizeCurrencyCode } from './helpers';
|
|
|
|
|
import type { CurrencyCode, CurrencyRatesProvider, RateEntry, RatesOptions } from './types';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a lazy rates provider backed by a per-pair cache table.
|
|
|
|
|
*/
|
|
|
|
|
export function createRates(opts: RatesOptions = {}): CurrencyRatesProvider {
|
|
|
|
|
const cache = new Map<string, RateEntry>();
|
|
|
|
|
const pendingFetches = new Map<string, Promise<number | undefined>>();
|
|
|
|
|
const now = opts.now ?? Date.now;
|
|
|
|
|
const { logger } = opts;
|
|
|
|
|
const diagnostics = createCurrencyDiagnostics(logger);
|
Build runtime infrastructure layer: aapp + 5 new artifacts
Adds the application composition root and six new runtime artifacts. lang
and logr alone could not cover an app surface — locale propagation, format
helpers, frontend preferences, validation, persistence and DOM service all
needed independent artifacts that aapp wires together under a single
locale source of truth and a shared logger.
New artifacts:
- aapp: composition root. createActiveApp + createTestApp +
App.createSiumEngine + frontend.persist + Storage wiring.
- stor: pluggable adapters (local/session/memory/cookie + SSR via
cookieAdapter.fromCookies), envelope versioning + migrate +
TTL, validate via Standard Schema, intra-tab + cross-tab sync,
Storage.clear(), per-entry adapter/namespace overrides.
- adom: reactive DOM service. shareViewport opt-in + per-window
tracker, dispose(). breakpoints, viewport, attribute writes.
- fend: frontend preferences (theme, mode, dir, density,
reducedMotion, reducedSound) with auto/clear/isAuto pattern.
- fmts: localized formatting (numbers, currency, units, dates) with
shared LocaleSource and per-domain auto/clear pattern.
- sium: validation engine. Standard Schema interop, Lang/Logger
injection, codec/refine/transform, domain types
(color/date/time), introspection.
Existing artifacts extended:
- lang: BCP 47 SupportedLocale (LangBase | base-region), mono lang
for monolingual apps, extend() leaf-overwrite warning,
EngineLang/ActiveLang naming alignment with the rest of the
project (no Instance suffix).
- logr: EngineLogger.dispose(), failureThrottleMs anti-cascade for
transports, README sync to the per-level filter API
(minLevel removed).
Cross-cutting:
- $locale: shared LocaleSource type consumed by fmts and fend.
- arts/README.md: artifact map, dependency graph, naming conventions.
- Test pages for every artifact under /test/<artifact>.
733 tests passing; svelte-check clean except 2 pre-existing errors on
sium/svelte provider (port pending).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
|
|
|
let cacheVersion = 0;
|
|
|
|
|
|
|
|
|
|
function pairKey(from: CurrencyCode, to: CurrencyCode): string {
|
|
|
|
|
return `${from}${RATE_PAIR_SEPARATOR}${to}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function splitPairKey(key: string): [CurrencyCode, CurrencyCode] {
|
|
|
|
|
const [from, to] = key.split(RATE_PAIR_SEPARATOR);
|
|
|
|
|
return [from, to];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isActive(entry: RateEntry): boolean {
|
|
|
|
|
return entry.expiresAt === Infinity || now() < entry.expiresAt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCachedEntry(from: CurrencyCode, to: CurrencyCode): RateEntry | undefined {
|
|
|
|
|
const key = pairKey(from, to);
|
|
|
|
|
const entry = cache.get(key);
|
|
|
|
|
if (entry === undefined) return undefined;
|
|
|
|
|
if (isActive(entry)) return entry;
|
|
|
|
|
cache.delete(key);
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setCachedRate(
|
|
|
|
|
from: CurrencyCode,
|
|
|
|
|
to: CurrencyCode,
|
|
|
|
|
rate: number,
|
|
|
|
|
expiresAt = Infinity
|
|
|
|
|
): void {
|
|
|
|
|
assertValidRate(from, to, rate);
|
|
|
|
|
if (from === to) return;
|
|
|
|
|
cache.set(pairKey(from, to), { rate, expiresAt });
|
|
|
|
|
cache.set(pairKey(to, from), { rate: 1 / rate, expiresAt });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCrossRate(from: CurrencyCode, to: CurrencyCode): number | undefined {
|
|
|
|
|
for (const [key, first] of cache) {
|
|
|
|
|
if (!isActive(first)) {
|
|
|
|
|
cache.delete(key);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [left, middle] = splitPairKey(key);
|
|
|
|
|
if (left !== from || middle === to) continue;
|
|
|
|
|
|
|
|
|
|
const second = getCachedEntry(middle, to);
|
|
|
|
|
if (second === undefined) continue;
|
|
|
|
|
|
|
|
|
|
const rate = first.rate * second.rate;
|
|
|
|
|
const expiresAt = Math.min(first.expiresAt, second.expiresAt);
|
|
|
|
|
setCachedRate(from, to, rate, expiresAt);
|
|
|
|
|
return rate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opts.initial) {
|
|
|
|
|
const { base, rates, expiresAt = Infinity } = opts.initial;
|
|
|
|
|
const normalizedBase = normalizeCurrencyCode(base);
|
|
|
|
|
for (const [currency, rate] of Object.entries(rates)) {
|
|
|
|
|
setCachedRate(normalizedBase, normalizeCurrencyCode(currency), rate, expiresAt);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
async getRate(from: CurrencyCode, to: CurrencyCode): Promise<number | undefined> {
|
|
|
|
|
from = normalizeCurrencyCode(from);
|
|
|
|
|
to = normalizeCurrencyCode(to);
|
|
|
|
|
if (from === to) return 1;
|
|
|
|
|
|
|
|
|
|
const direct = getCachedEntry(from, to);
|
|
|
|
|
if (direct !== undefined) return direct.rate;
|
|
|
|
|
|
|
|
|
|
const cross = getCrossRate(from, to);
|
|
|
|
|
if (cross !== undefined) return cross;
|
|
|
|
|
|
|
|
|
|
if (!opts.fetchRate) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fetchKey = pairKey(from, to);
|
|
|
|
|
if (pendingFetches.has(fetchKey)) {
|
|
|
|
|
return pendingFetches.get(fetchKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fetchVersion = cacheVersion;
|
|
|
|
|
const promise = opts
|
|
|
|
|
.fetchRate(from, to)
|
|
|
|
|
.then((result) => {
|
|
|
|
|
pendingFetches.delete(fetchKey);
|
|
|
|
|
if (fetchVersion !== cacheVersion) return undefined;
|
|
|
|
|
if (!result) return undefined;
|
|
|
|
|
if (!isValidRate(result.rate)) {
|
|
|
|
|
emitCurrencyDiagnostic(diagnostics, CURRENCY_DIAGNOSTIC_EVENTS.INVALID_RATE, {
|
|
|
|
|
from,
|
|
|
|
|
to,
|
|
|
|
|
rate: result.rate
|
Build runtime infrastructure layer: aapp + 5 new artifacts
Adds the application composition root and six new runtime artifacts. lang
and logr alone could not cover an app surface — locale propagation, format
helpers, frontend preferences, validation, persistence and DOM service all
needed independent artifacts that aapp wires together under a single
locale source of truth and a shared logger.
New artifacts:
- aapp: composition root. createActiveApp + createTestApp +
App.createSiumEngine + frontend.persist + Storage wiring.
- stor: pluggable adapters (local/session/memory/cookie + SSR via
cookieAdapter.fromCookies), envelope versioning + migrate +
TTL, validate via Standard Schema, intra-tab + cross-tab sync,
Storage.clear(), per-entry adapter/namespace overrides.
- adom: reactive DOM service. shareViewport opt-in + per-window
tracker, dispose(). breakpoints, viewport, attribute writes.
- fend: frontend preferences (theme, mode, dir, density,
reducedMotion, reducedSound) with auto/clear/isAuto pattern.
- fmts: localized formatting (numbers, currency, units, dates) with
shared LocaleSource and per-domain auto/clear pattern.
- sium: validation engine. Standard Schema interop, Lang/Logger
injection, codec/refine/transform, domain types
(color/date/time), introspection.
Existing artifacts extended:
- lang: BCP 47 SupportedLocale (LangBase | base-region), mono lang
for monolingual apps, extend() leaf-overwrite warning,
EngineLang/ActiveLang naming alignment with the rest of the
project (no Instance suffix).
- logr: EngineLogger.dispose(), failureThrottleMs anti-cascade for
transports, README sync to the per-level filter API
(minLevel removed).
Cross-cutting:
- $locale: shared LocaleSource type consumed by fmts and fend.
- arts/README.md: artifact map, dependency graph, naming conventions.
- Test pages for every artifact under /test/<artifact>.
733 tests passing; svelte-check clean except 2 pre-existing errors on
sium/svelte provider (port pending).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
|
|
|
});
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
setCachedRate(from, to, result.rate, result.expiresAt ?? Infinity);
|
|
|
|
|
return result.rate;
|
|
|
|
|
})
|
|
|
|
|
.catch((error: unknown) => {
|
|
|
|
|
pendingFetches.delete(fetchKey);
|
|
|
|
|
if (fetchVersion !== cacheVersion) return undefined;
|
|
|
|
|
emitCurrencyDiagnostic(diagnostics, CURRENCY_DIAGNOSTIC_EVENTS.RATE_FETCH_FAILED, {
|
|
|
|
|
from,
|
|
|
|
|
to,
|
Build runtime infrastructure layer: aapp + 5 new artifacts
Adds the application composition root and six new runtime artifacts. lang
and logr alone could not cover an app surface — locale propagation, format
helpers, frontend preferences, validation, persistence and DOM service all
needed independent artifacts that aapp wires together under a single
locale source of truth and a shared logger.
New artifacts:
- aapp: composition root. createActiveApp + createTestApp +
App.createSiumEngine + frontend.persist + Storage wiring.
- stor: pluggable adapters (local/session/memory/cookie + SSR via
cookieAdapter.fromCookies), envelope versioning + migrate +
TTL, validate via Standard Schema, intra-tab + cross-tab sync,
Storage.clear(), per-entry adapter/namespace overrides.
- adom: reactive DOM service. shareViewport opt-in + per-window
tracker, dispose(). breakpoints, viewport, attribute writes.
- fend: frontend preferences (theme, mode, dir, density,
reducedMotion, reducedSound) with auto/clear/isAuto pattern.
- fmts: localized formatting (numbers, currency, units, dates) with
shared LocaleSource and per-domain auto/clear pattern.
- sium: validation engine. Standard Schema interop, Lang/Logger
injection, codec/refine/transform, domain types
(color/date/time), introspection.
Existing artifacts extended:
- lang: BCP 47 SupportedLocale (LangBase | base-region), mono lang
for monolingual apps, extend() leaf-overwrite warning,
EngineLang/ActiveLang naming alignment with the rest of the
project (no Instance suffix).
- logr: EngineLogger.dispose(), failureThrottleMs anti-cascade for
transports, README sync to the per-level filter API
(minLevel removed).
Cross-cutting:
- $locale: shared LocaleSource type consumed by fmts and fend.
- arts/README.md: artifact map, dependency graph, naming conventions.
- Test pages for every artifact under /test/<artifact>.
733 tests passing; svelte-check clean except 2 pre-existing errors on
sium/svelte provider (port pending).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
|
|
|
error
|
|
|
|
|
});
|
|
|
|
|
return undefined;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
pendingFetches.set(fetchKey, promise);
|
|
|
|
|
return promise;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setRate(from: CurrencyCode, to: CurrencyCode, rate: number, expiresAt = Infinity) {
|
|
|
|
|
setCachedRate(normalizeCurrencyCode(from), normalizeCurrencyCode(to), rate, expiresAt);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setRates(base: CurrencyCode, rates: Record<CurrencyCode, number>, expiresAt = Infinity) {
|
|
|
|
|
const normalizedBase = normalizeCurrencyCode(base);
|
|
|
|
|
for (const [currency, rate] of Object.entries(rates)) {
|
|
|
|
|
setCachedRate(normalizedBase, normalizeCurrencyCode(currency), rate, expiresAt);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
hasRates(): boolean {
|
|
|
|
|
for (const [key, entry] of cache) {
|
|
|
|
|
if (isActive(entry)) return true;
|
|
|
|
|
cache.delete(key);
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
clear(): void {
|
|
|
|
|
cacheVersion++;
|
|
|
|
|
cache.clear();
|
|
|
|
|
pendingFetches.clear();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type { RateFetcher, RatesOptions } from './types';
|