Bloque I3+I4 — format LRU cap + unified locale source

I3 — `Intl.NumberFormat` cache in currency now caps at 256 entries with
LRU eviction. The cache was module-global and unbounded; long-running
multi-locale / multi-currency apps (financial dashboards, i18n test
matrices) accumulated formatter instances forever. Map iteration order
is insertion-order so the LRU is implemented as "delete on hit, set on
hit, evict the first key when full" — no extra structure.

I4 — `createActiveFormatLocaleSource` now maintains its own listener
set, so `setLocale()` fires every subscribing submodule through one
notification. Previously the parent's `setLocale` only mutated the
internal `currentLocale` and the parent then re-called `setLocale` on
every submodule manually — two notifications per change. The unified
source delivers exactly one. `createActiveFormat.setLocale()` no
longer needs to fan out to numbers/currency/units/dates by hand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
master
dev 5 months ago
parent 7fb071d22e
commit b1e73f4586

@ -59,11 +59,11 @@ export function createActiveFormat(options: ActiveFormatOptions = {}): ActiveFor
function setLocale(nextLocale: string): void {
if (disposed) return;
// Single notification: localeState.setLocale fans out to every
// submodule through `source.onLocaleChange` (each submodule
// subscribed when `createActiveX({ localeSource })` ran). No need
// to call setLocale on each one explicitly.
localeState.setLocale(nextLocale);
numbers.setLocale(nextLocale);
currency.setLocale(nextLocale);
units.setLocale(nextLocale);
dates.setLocale(nextLocale);
}
return {

@ -30,22 +30,39 @@ export function createActiveFormatLocaleSource(
options: ActiveFormatLocaleSourceOptions
): ActiveFormatLocaleSource {
let currentLocale = options.localeSource?.getLocale() ?? options.locale;
// Single fan-out point: every submodule (numbers/currency/units/dates)
// subscribes through `source.onLocaleChange`, and this set is also
// fired when the parent calls `setLocale()` directly. Previously the
// parent's `setLocale` only mutated `currentLocale` and the parent
// then re-called `setLocale` on every submodule manually — two
// notifications per change. The unified source delivers exactly one.
const listeners = new Set<(locale: string) => void>();
function getLocaleNow(): string {
return currentLocale ?? options.localeSource?.getLocale() ?? options.locale ?? '';
}
const source: FormatLocaleSource = {
getLocale: () => currentLocale ?? options.localeSource?.getLocale() ?? options.locale ?? '',
getLocale: getLocaleNow,
onLocaleChange: (fn) => {
const unsubscribe = options.localeSource?.onLocaleChange?.((locale) => {
listeners.add(fn);
const unsubscribeUpstream = options.localeSource?.onLocaleChange?.((locale) => {
currentLocale = locale;
fn(locale);
for (const listener of listeners) listener(locale);
});
return () => unsubscribe?.();
return () => {
listeners.delete(fn);
unsubscribeUpstream?.();
};
}
};
return {
source,
getLocale: source.getLocale,
getLocale: getLocaleNow,
setLocale(locale) {
currentLocale = locale;
for (const listener of listeners) listener(locale);
}
};
}

@ -18,6 +18,24 @@ import type {
EngineCurrencyOptions
} from './types';
/**
* Module-global LRU cap for `Intl.NumberFormat` instances. The
* underlying `Intl.NumberFormat` is heavy to construct (multi-locale
* apps observed 5-10ms per first build), but grows unbounded if every
* locale × currency × option combination is unique.
*
* The cap is intentionally generous — the typical app uses < 30 unique
* keys; the limit guards long-running apps that pivot through many
* locales/currencies (e.g. financial dashboards) without paying memory
* for combinations that haven't been used in a while.
*/
const FORMAT_CACHE_LIMIT = 256;
/**
* `Map`-based LRU: the iteration order of a `Map` is insertion order;
* a hit re-inserts the key so it's the freshest. When the cap is
* exceeded, we drop the oldest key (the first one yielded by `keys()`).
*/
const formatCache = new Map<string, Intl.NumberFormat>();
function formatKey(locale: string, currency: CurrencyCode, options: CurrencyFormatOptions): string {
@ -31,7 +49,12 @@ function getCurrencyFormatter(
): Intl.NumberFormat {
const key = formatKey(locale, currency, options);
const cached = formatCache.get(key);
if (cached !== undefined) return cached;
if (cached !== undefined) {
// Promote on hit.
formatCache.delete(key);
formatCache.set(key, cached);
return cached;
}
const formatter = new Intl.NumberFormat(locale || undefined, {
style: 'currency',
@ -44,6 +67,13 @@ function getCurrencyFormatter(
signDisplay: options.signDisplay,
useGrouping: options.useGrouping
});
// Evict the oldest entry when we exceed the cap. Map's iteration
// order is insertion-order so `.keys().next().value` is the LRU key.
if (formatCache.size >= FORMAT_CACHE_LIMIT) {
const oldest = formatCache.keys().next().value;
if (oldest !== undefined) formatCache.delete(oldest);
}
formatCache.set(key, formatter);
return formatter;
}

Loading…
Cancel
Save

Powered by TurnKey Linux.