import type { IResettableModel } from '../models/types.js'; /** Represents a lazily loaded value that initializes on first access. */ export interface ILazy { /** Returns current value, triggering loading if not yet loaded. */ readonly value: T; /** * Returns true if a value of type `T` has been successfully loaded (no error). * * When `true`, `value` is guaranteed to be `T` (not `TInitial` or an error fallback). * When `false`, `value` may be `TInitial`, `undefined`, or a stale value from a previous successful load. * * Does not trigger loading. */ readonly hasValue: boolean; /** Returns current value or undefined if not loaded. Does not trigger loading. */ readonly currentValue: T | undefined; /** Returns the raw error if loading failed, null otherwise. Does not trigger loading. */ readonly error: unknown; /** * Returns error message (string) if loading failed, null otherwise. Does not trigger loading. * @deprecated Use {@link error} instead — it preserves the original error for typed handling. * If you need a display string, format the error at the presentation layer. */ readonly errorMessage: string | null; } /** Represents a lazily asynchronously loaded value with promise-based access. */ export interface ILazyPromise extends ILazy { /** * Returns loading state: true (loading), false (loaded), null/undefined (not started). * Does not trigger loading. */ readonly isLoading: boolean | null | undefined; /** * Returns the promise for the value, triggering loading if not started. * * On error, resolves to the current value (stale or initial) instead of rejecting. */ readonly promise: Promise; /** * Re-executes the factory to get fresh data. If concurrent refreshes occur, the latest wins. * All awaiting promises will resolve to the final refreshed value. * * On error, resolves to the current value (stale or initial) instead of rejecting. * * **⚠️ Use sparingly:** Only refresh when explicitly needed for fresh data. * Over-use defeats lazy loading and caching benefits. * * **Valid use cases:** * - User-initiated refresh (pull-to-refresh, refresh button) * - Cache invalidation after mutation * - Time-based refresh with throttling * - Error recovery * * **Avoid:** * - Refreshing on every render/mount * - Using instead of cache expiration (use `withExpire`) * - Calling in loops or high-frequency events without debouncing * * @returns Promise resolving to the refreshed value, or the current value on error */ refresh(): Promise; /** * Type-narrowing check: returns `true` if the value has been successfully resolved to `T`. * * When this returns `true`, `value` and `currentValue` are narrowed to `T` (not `TInitial`). * * @example * ```typescript * const lazy: ILazyPromise = cache.getLazy('user-1'); * if (lazy.hasResolvedValue()) { * // lazy.value is `User` here, not `User | undefined` * console.log(lazy.value.name); * } * ``` */ hasResolvedValue(): this is IResolvedLazyPromise; } /** Narrowed state of ILazyPromise after successful resolution. */ export interface IResolvedLazyPromise extends ILazyPromise { readonly value: T; readonly currentValue: T; readonly hasValue: true; readonly isLoading: false; readonly error: null; } /** * Controllable lazy promise with manual state management. * Extends ILazyPromise with methods to manually set values and reset state. */ export interface IControllableLazyPromise extends ILazyPromise, IResettableModel { /** * Manually sets the value and marks loading as complete. * Useful for cache synchronization and manual state updates. * * @param value - The value to set * @returns The value that was set */ setInstance(value: T): T; } /** * Factory function that retrieves the value for LazyPromise. * * @param refreshing - True when called via refresh(), false on initial load */ export type LazyFactory = (refreshing?: boolean) => Promise; /** * Extension for LazyPromise instances, enabling factory wrapping and instance augmentation. * * @template T - Value type the extension is compatible with (use `any` for universal extensions) * @template TExtShape - Additional properties/methods added to the instance * * @example * ```typescript * // Universal logging extension * const loggingExtension: ILazyPromiseExtension = { * overrideFactory: (original) => async (refreshing) => { * console.log('Loading...'); * return await original(refreshing); * } * }; * ``` */ export interface ILazyPromiseExtension { /** * Augment the instance with additional properties/methods. * Receives IControllableLazyPromise with setInstance() and reset() for manual control. * * @param previous - The controllable LazyPromise instance * @returns The instance with additional shape */ extendShape?: (previous: IControllableLazyPromise) => IControllableLazyPromise & TExtShape; /** * Wrap or replace the factory function. * * @param original - The original factory function * @param target - The LazyPromise instance being extended * @returns A new factory function */ overrideFactory?: (original: LazyFactory, target: ILazyPromise & TExtShape) => LazyFactory; /** * Cleanup function called when the LazyPromise is disposed. * Use for cleaning up resources (timers, subscriptions, listeners). * Executes in reverse order: newest extension first, oldest last. * * @param instance - The extended LazyPromise instance being disposed * * @example * ```typescript * const intervalExtension: ILazyPromiseExtension void }> = { * extendShape: (instance) => { * let intervalId: NodeJS.Timeout | null = null; * return Object.assign(instance, { * stopTimer: () => { if (intervalId) clearInterval(intervalId); } * }); * }, * dispose: (instance) => instance.stopTimer() * }; * ``` */ dispose?: (instance: ILazyPromise & TExtShape) => void; }