Lazy values
Memoised deferred values. The flagship utility of the package — the unique tool for breaking ES-module circular imports.
import { lazy, isLazy, type Lazy, type LazyAsync } from "@mongez/reinforcements";lazy(producer)
lazy<T>(producer: () => T): Lazy<T>
type Lazy<T> = { resolve(): T; // compute (once) and return the cached value reset(): void; // drop the cache; next resolve() recomputes isResolved(): boolean; // has resolve() been called? peek(): T | undefined; // cached value without forcing computation};The producer is not invoked until the first resolve(). The result is then memoised.
const config = lazy(() => loadHeavyConfig());
config.resolve(); // computesconfig.resolve(); // cached, no recomputationconfig.reset(); // forget cached valueconfig.resolve(); // recomputesconfig.peek(); // returns cached value or undefinedconfig.isResolved(); // true / falseWhy it exists: circular imports
JavaScript closures capture variable bindings, not values. When Module A imports Module B which imports Module A, the value that A wants from B might not be defined yet at module-init time. lazy() defers the reference resolution to call time.
// In a module that creates a circular dep:const service = lazy(() => Service); // Service is undefined right now — fine
export function handler() { return service.resolve().run(); // Service is guaranteed to exist by this point}lazy.async(producer)
lazy.async<T>(producer: () => Promise<T>): LazyAsync<T>
type LazyAsync<T> = { resolve(): Promise<T>; reset(): void; isResolved(): boolean; peek(): Promise<T> | undefined;};Same shape as lazy, but the cached value is a Promise<T>.
const user = lazy.async(() => fetch("/api/me").then(r => r.json()));
await user.resolve(); // fetchesawait user.resolve(); // returns the same cached promise — no refetchuser.reset();await user.resolve(); // refetcheslazy.from(value)
lazy.from<T>(value: T): Lazy<T>Pre-resolved lazy — for tests or for API symmetry where a Lazy<T> is expected but the value is already known.
const ref = lazy.from(42);ref.resolve(); // 42ref.isResolved(); // trueref.reset(); // no-op for pre-resolved laziesisLazy(value)
isLazy<T = unknown>(value: unknown): value is Lazy<T>Type guard. Returns true for both lazy() and lazy.async() results.
if (isLazy<number>(value)) { value.resolve(); // typed as number}Idioms
Resetting on config reload:
const cachedConfig = lazy(() => parseConfigFile());
watchConfigFile(() => { cachedConfig.reset();});
export function getSetting(key: string) { return cachedConfig.resolve()[key]; // re-parses on first call after reload}Disambiguating “not yet resolved” from “resolved to undefined”:
const maybe = lazy(() => undefined);
maybe.isResolved(); // falsemaybe.peek(); // undefinedmaybe.resolve();maybe.isResolved(); // truemaybe.peek(); // undefined ← but isResolved() distinguishes the casesError retry: producers that throw are not memoised; the next resolve() re-invokes the producer.