import { IObservableArray } from "mobx"; //#region src/lazy/lazy.d.ts /** Options for `invalidate()`. */ interface LazyInvalidateOptions { /** * Drop the current value instead of keeping it readable until the refetch lands. * Default `false`: a refresh keeps showing what it already has. */ discard?: boolean; } interface LazyApi { /** * How the last request ended, or `undefined` if it succeeded (or none has run). Cleared when a * new request starts and on every success. * * An error does **not** clear the value: a failed refresh keeps showing what it had, so `error` * and a readable `value` coexist. Check `loaded` to decide whether there is anything to render; * `error` only tells you what happened last. */ error: unknown; /** * `true` whenever a request is in flight, including a background refresh that is still showing * its previous value. * * Orthogonal to `loaded`, deliberately: the two together describe every state without overlapping. * A first load is `!loaded && fetching`; a refresh is {@link LazyApi.refreshing}. * * ⚠️ **Reading this does not observe the lazy.** It is not one of the observation sources, so a * render that decides what to show from `fetching` alone subscribes to nothing — see * {@link LazyApi.refreshing}, which is the safe way to ask the same question. */ fetching: boolean; /** * A request is in flight behind a value that is already there — a refresh, as opposed to a first * load. Exactly `loaded && fetching`. * * **Prefer this to a bare `fetching` for anything that decides what to render.** Reading it * touches whether there *is* a value, which is one of the reads that marks a lazy observed, so a * placeholder branch gated on it keeps the lazy alive. A branch gated on `fetching` alone * observes nothing: the lazy is dropped, its load aborted, `fetching` cleared — and the branch * renders itself away again, as fast as the event loop allows. Development warns once when it * sees that happening. * * (The `loading` property this resembles was removed for a different reason: it read as the * opposite of `loaded` while actually meaning "a request is in flight", and mishandled a failed * first load. This one is a conjunction of the two facts rather than a substitute for either.) */ refreshing: boolean; /** * When a request last succeeded, as epoch milliseconds, or `undefined` if none ever has. * * Named for the fetch rather than the value, because the two differ: a lazy seeded with * `initialValue` is `loaded` with no `fetchedAt` (hydrated, never been to the network), and a * failed refresh leaves the previous timestamp in place (still showing data from then). */ fetchedAt: number | undefined; /** * `true` while at least one reaction is observing `value`, `loaded` or `error`. * Observable, so it can be read reactively — reading it does not itself count as observing the * value, so it never triggers a load. */ observed: boolean; /** * Resolve with the current value, loading first if there isn't one *or* the one held is stale. * Joins a load already in flight rather than starting a second. * * Staleness counts, so `invalidate()` followed by `getOrLoad()` fetches rather than handing back * the value being replaced — whether or not anything happens to be observing. */ getOrLoad(): Promise; /** Always start a fresh load, abandoning any result already in flight. */ reload(): Promise; /** * Write the value directly and mark it loaded and fresh, without fetching — the value is treated * as authoritative, so no load is owed. Abandons any load in flight (so it cannot clobber this * write) and detaches from dependency-driven refetching until the next load. * * Contrast `initialValue`, which is loaded but still *stale*: a starting point that gets * revalidated on first observation. */ set(value: T): void; /** * Mark the value stale and load again *if anyone is watching*. If nothing is observing, the load * happens on next observation — or on the next `getOrLoad()`, which counts staleness. * * The current value stays readable while the refetch runs (`loaded` remains `true`, `fetching` * becomes `true`), so a list doesn't blank out between a mutation and its refresh. Pass * `{ discard: true }` to drop it first and show a fresh load instead. */ invalidate(options?: LazyInvalidateOptions): void; } /** * A lazy observable. * * `loaded` is a discriminant, so checking it narrows `value` — no separate `!== undefined` guard: * * ```ts * if (list.loaded) list.value.map(render); // value is T here * ``` * * Written as a union of two complete members rather than `Api & (A | B)`. Both narrow — TypeScript * distributes the intersection — but `LazyArray` cannot be: it has to `Omit` `set` from * the API and replace it, and `Omit` over a union collapses it into one object, taking the * discriminant with it. The two types are spelled the same way so they stay comparable. */ type Lazy = (LazyApi & { loaded: true; value: T; }) | (LazyApi & { loaded: false; value: undefined; }); /** * The `loaded: true` arm of {@link Lazy} — a lazy that is known to hold a value, so * `value` reads as `T` with no check. What a seeded `lazy` hands back, and what a * `loaded` check narrows an ordinary one to. */ type LoadedLazy = Extract, { loaded: true; }>; interface LazyOptions { /** * Whether the value's contents are made observable recursively, as in mobx's own `deep` option. * Defaults to `true`. Pass `false` for values that manage their own observability — model * instances, for one — so nothing is converted on the way in. */ deep?: boolean; /** * How long a loaded value outlives its last observer. `false` (the default) drops it as soon * as nothing is watching, `true` keeps it forever, and `{ for: ms }` keeps it that long before * dropping it — useful to survive a quick unmount/remount without refetching. * * Errors are never kept, regardless of this setting. */ keepOnUnobserved?: boolean | { for: number; }; /** * Re-run `fetch` when observables it read while running change — a filter, a session field, * a parent model's id. * * `false` (the default) calls `fetch` exactly once per load, so an observable it happens to * touch can never trigger a request you didn't ask for. `true` tracks its reads and refetches * on change. `{ throttle: ms }` tracks and allows at most one refetch per window, so a burst of * changes (a filter bound to a text input) costs one request rather than one per keystroke. * The first load is never throttled. * * Note this throttles rather than debounces: the window opens at the first change and is not * pushed back by later ones, so sustained changes still refresh once per window instead of * waiting for them to stop. * * Each re-run supersedes the previous request and aborts its signal. */ trackDependencies?: boolean | { throttle: number; }; /** * Refresh the value automatically this often, in milliseconds — for data that should not go * stale on screen (a dashboard, a queue, a status board). * * Only runs while something is observing: an unobserved lazy has nobody to refresh for. The * interval is measured from the last completed request rather than a fixed clock, so a slow * response pushes the next reload out instead of stacking requests. Coming back into * observation after longer than the interval refreshes immediately. * * The current value stays readable throughout, exactly as with `invalidate()`. A failed * reload reports its error and is retried on the next interval. */ reloadEvery?: number; debugName?: string; } interface LazyOptionsWithInitialValue extends LazyOptions { /** * A value to start with, before anything is fetched. The lazy reports `loaded` immediately and * still counts as stale, so the first observation revalidates it — which is what makes this the * right shape for hydration from SSR, storage, or a cache you already trust. * * Without it a lazy holds nothing (`loaded: false`, `value: undefined`) until a load lands. That * distinction is the point: an empty array means "there are none", not "not known yet". * * Passing this narrows the result to {@link LoadedLazy}, so `value` reads as `T` * without a `loaded` check — the seed is restored by a discard, so a seeded lazy can never go * back to holding nothing. * * Presence is what counts, not the value: for a `T` that includes `undefined`, seeding with * `undefined` is a real value and reports `loaded`, matching a fetch that resolves `undefined`. * That is why the unseeded overload has no `initialValue` at all — `{ initialValue: maybe }` * where `maybe` might be `undefined` is the one state that cannot be represented, so it is * rejected at the call site rather than guessed at. */ initialValue?: T; } /** * What a fetch is handed. An object rather than a bare signal so more can be added later without * breaking every fetcher — and so a client whose own first parameter is an options bag can be * attached directly. */ interface LazyFetchOptions { /** * Aborts as soon as the request is superseded — by `reload`, `set`, `invalidate`, going * unobserved, or a dependency change. */ signal: AbortSignal; } /** Taking the argument is optional; zero-argument fetchers remain valid. */ type LazyFetch = (options: LazyFetchOptions) => Promise; declare function lazy(fetch: LazyFetch, options: LazyOptionsWithInitialValue & { initialValue: T; }): LoadedLazy; declare function lazy(fetch: LazyFetch, options?: LazyOptions): Lazy; /** * The API of a list lazy: everything a scalar one has, except that `set` takes a plain array — * callers hand over data, not an observable container. */ type LazyArrayApi = Omit>, "set"> & { set(value: T[]): void; }; /** * A lazy over a list. `value` is the *same* observable array for the lifetime of the lazy once * there is one — loads replace its contents rather than the array — so a reference you hold stays * valid. The trade is that the identity is no longer a change signal: observe the contents, or * `fetchedAt`. * * `value` is `undefined` until the first load (or an explicit `initialValue`), because "no rows * yet" and "zero rows" are different answers and only one of them is a fact. `loaded` narrows it, * exactly as it does for a scalar lazy. */ type LazyArray = (LazyArrayApi & { loaded: true; value: IObservableArray; }) | (LazyArrayApi & { loaded: false; value: undefined; }); /** * The `loaded: true` arm of {@link LazyArray} — the list counterpart of * {@link LoadedLazy}. What a seeded `lazyArray` hands back, including one * seeded with `[]`: "there are none" is a fact, and a fact is loaded. */ type LoadedLazyArray = Extract, { loaded: true; }>; interface LazyArrayOptions extends LazyOptions { /** * Rows to start with — see {@link LazyOptionsWithInitialValue.initialValue}, of which * this is the list form. Passing it narrows the result to {@link LoadedLazyArray}. * * Unlike the scalar case there is nothing ambiguous to guard against, because `undefined` is * never a list: `{ initialValue: maybeRows }` is accepted and simply does not narrow, since a * seed that might not be there cannot promise a value. */ initialValue?: T[]; } declare function lazyArray(fetch: LazyFetch, options: LazyArrayOptions & { initialValue: T[]; }): LoadedLazyArray; declare function lazyArray(fetch: LazyFetch, options?: LazyArrayOptions): LazyArray; /** * What a page fetch is handed: the lazy's own options, plus where in the list this request is. * * Both `cursor` and `offset` are supplied on every request, so the same shape serves a * cursor-paginated endpoint and an offset-paginated one and you use whichever your API speaks. */ interface LazyPageRequest extends LazyFetchOptions { /** The cursor the previous page reported, or `undefined` for the first page of a run. */ cursor: string | undefined; /** How many rows are already held — the offset an offset-paginated endpoint wants. `0` on a first page. */ offset: number; /** How many rows to ask for: whatever `pageSize` is set to. */ limit: number; /** Zero-based index of the page being fetched. `0` for a first load, a reload, or a query change. */ page: number; /** Whatever `setQuery` was last given — the filters and sorts, for a table-driven list. */ query: Q; } /** * What a page fetch resolves to. * * A bare array is the whole answer for an endpoint that returns rows and nothing else. The * envelope carries whatever else it knows, and every field is optional because most endpoints * report one or two of them rather than all three — see {@link LazyPagesApi.hasMore} for how they * combine. */ type LazyPageResult = T[] | { items: T[]; /** * Cursor for the *next* page. `null` means this page was the last, which is why the field * being **present** is what makes it authoritative: an absent `cursor` says nothing. */ cursor?: string | null; /** Total rows matching the query across every page — the "of 4,382" in a row count. */ total?: number; /** Whether another page exists, for an endpoint that says so outright. Outranks the rest. */ hasMore?: boolean; }; type LazyPagesFetch = (request: LazyPageRequest) => Promise>; /** * Options for `lazyPages`. A deliberate subset of {@link LazyOptions}, and the two that are * missing are missing for reasons rather than oversight: * * - **`initialValue`** — a seed cannot say which cursor follows it, so it could be listed but * never continued. Hydrating the first page is `set(rows)`, which says the same thing honestly: * these rows, and nothing after them. * - **`reloadEvery`** — a reload starts the list over at page one, so polling would yank a user * who had scrolled to page eight back to the top on a timer. Refresh a paged list on an event * (`invalidate()`), not on a clock. */ interface LazyPagesOptions extends Pick { /** * Rows per request, sent to the fetch as `limit`. Default 50. * * It doubles as the fallback for `hasMore` when an endpoint reports nothing else — a page * shorter than this is the last one — so it should match what the server actually returns. */ pageSize?: number; /** The query the first page is fetched with, before any {@link LazyPagesApi.setQuery}. */ query?: Q; /** * A row's identity, used to drop a record that a later page repeats. * * Worth setting for anything served by cursor over a non-unique sort key, or by offset while * rows are being inserted: both hand back a record already held, and the duplicate is not * harmless — a table keys its rows by identity, so two entries for one record produce two rows * that share a React key and a single selection toggle that hits both. * * For a model-backed list, `dedupeBy: SurveyModel.identityKey` is exactly this. */ dedupeBy?: (item: T) => unknown; } /** * Everything a {@link LazyArray} has, plus what only an accumulating list can answer. * * Note which of these observe the list and which do not. `value`, `loaded`, `error` and * `loadingMore` do, so a render gated on any of them keeps the list alive and triggers its first * load. `hasMore`, `total`, `pages` and `fetching` do **not** — they describe the requests rather * than the rows, so a footer that renders "1–100 of 4,382" can't pin a list in memory or start a * fetch just by being on screen. It is the same split, and the same reasoning, as * {@link LazyApi.fetching} versus {@link LazyApi.refreshing}. */ interface LazyPagesApi extends LazyArrayApi { /** * Append the next page, resolving with the list once it lands. * * Safe to call speculatively, which is what makes it usable as a scroll handler: it resolves * immediately when there is nothing more, and **joins** a request already in flight rather than * starting a second one — so a burst of scroll events costs one page, not one each. * * On a list that holds nothing yet this fetches the first page, which is the same thing it * always is: the page after the ones held. */ loadMore(): Promise>; /** * Fetch every remaining page, one after another, and resolve with the whole list. * * For "Load all 2,000" rather than "load more" — a bar that offers the rest of the dataset in one * click, or an export that needs it in memory. Pages arrive as they land, so a row count bound to * `value.length` and `total` counts up throughout, and `loadingMore` stays `true` for the * duration. * * A hand-written `while (list.hasMore) await list.loadMore()` does the same thing in the happy * path. What this adds is **stopping**, in the two cases that loop cannot see: * * - **The list restarted underneath it.** A `setQuery`, a `reload`, a `created` event: the loop * would carry on and walk the *new* query to its end, which nobody asked for. This stops as * soon as a page fails to extend the list. * - **Nothing is watching any more.** If something was observing when the walk began — a mounted * table — and has stopped by the time a page lands, the view it was for is gone and the rest of * the walk is pure waste. A walk that began with nothing observing keeps going, because that is * a script asking on its own behalf, exactly as `getOrLoad()` fetches for one. * * Rejects if a page does, leaving the pages already loaded in place — so a retry continues from * where it stopped rather than starting over. */ loadAll(): Promise>; /** * A page request is in flight *behind rows already held* — the append counterpart of * {@link LazyApi.refreshing}, and mutually exclusive with it. * * Reading it observes the list, so this is the one to gate a footer spinner on. The two never * overlap: a reload replaces the list and reports `refreshing`, a `loadMore` extends it and * reports this. */ readonly loadingMore: boolean; /** * Whether another page exists. `true` before anything has loaded — the first page is a page. * * Resolved from what the last page reported, in this order: * * 1. An empty page ends the list, whatever else it says. Trusting `hasMore: true` alongside zero * rows is what turns a server bug into a request loop, and anything driving `loadMore()` off * a scroll position would spin it as fast as the event loop allows. * 2. An explicit `hasMore`. * 3. A `cursor` field, if the envelope carried one: `null` means the end. * 4. A `total`, if one has been reported: whether the rows held reach it. * 5. Otherwise a short page is the last page. An endpoint whose final page happens to be exactly * `pageSize` long therefore costs one extra request, which comes back empty and ends it. */ readonly hasMore: boolean; /** Total rows matching the query, if a page reported one. Survives appends; cleared by a query change. */ readonly total: number | undefined; /** * How many pages are held. `0` before the first lands, and back to `0` on anything that starts * the list over — a reload, a query change, a discard. * * That makes it the signal for "this list restarted" as opposed to "this list grew", which * nothing else here provides: the array identity is stable by design, so it cannot say. */ readonly pages: number; /** Whatever `setQuery` was last given, or the `query` option. */ readonly query: Q; /** * Point the list at a different query — filters, sorts, a search term. * * The query decides which rows exist, so this is not a refresh of the rows held: the list goes * stale from page one and reloads now if anything is watching, or on the next observation if * not. The rows stay readable throughout, so a table keeps showing the previous results until * the new first page lands rather than blanking. * * A structurally equal query is a no-op, so this is safe to call from an effect that runs more * often than the query changes. */ setQuery(query: Q): void; } /** * A lazy over a list that grows a page at a time. * * It is a {@link LazyArray} in every respect that matters to something reading rows — one stable * observable array, `undefined` until the first page lands, `loaded` narrowing it, loading on * first observation and dropping when nothing watches — so anything that accepts a `LazyArray` * accepts one of these, the table's `data` included. * * What it adds is the distinction a single-fetch lazy structurally cannot draw: `reload()` starts * the list over, `loadMore()` extends it, and `refreshing` / `loadingMore` say which is running. */ type LazyPages = (LazyPagesApi & { loaded: true; value: IObservableArray; }) | (LazyPagesApi & { loaded: false; value: undefined; }); /** * An accumulating list: one lazy, fetched a page at a time, for a dataset too large to hand over * whole. * * ```ts * const feed = lazyPages(({ cursor, limit, signal }) => * api.listSurveys({ cursor, limit, signal }), * ); * * feed.value; // undefined until the first page lands, then the accumulated rows * feed.loadMore(); // append the next page * feed.hasMore; // whether there is one * ``` * * Everything about *when* it loads is `lazy`'s: the first page is fetched when something observes * the list, requests abort when superseded, `keepOnUnobserved` decides how long the pages outlive * their last observer, and `trackDependencies` makes an observable the fetch reads a reason to * start the list over. Only the accumulation is new. * * **Three operations, deliberately distinct**, where a single-fetch lazy only has room for two: * * | | | * | --- | --- | * | `loadMore()` | the page after the ones held, appended | * | `reload()` | the first page again, replacing everything — the whole list, refetched | * | `setQuery(q)` | a different list; page one of it, rows held until it lands | */ declare function lazyPages(fetch: LazyPagesFetch, options?: LazyPagesOptions): LazyPages; /** * The value type a lazy resolves to. Inferred off `getOrLoad` rather than the type itself, so it * reads through the `loaded` union without needing to match either member. */ type InferLazy = O extends { getOrLoad(): Promise; } ? T : never; //#endregion export { lazyPages as S, LazyPagesOptions as _, LazyArrayApi as a, lazy as b, LazyFetchOptions as c, LazyOptionsWithInitialValue as d, LazyPageRequest as f, LazyPagesFetch as g, LazyPagesApi as h, LazyArray as i, LazyInvalidateOptions as l, LazyPages as m, Lazy as n, LazyArrayOptions as o, LazyPageResult as p, LazyApi as r, LazyFetch as s, InferLazy as t, LazyOptions as u, LoadedLazy as v, lazyArray as x, LoadedLazyArray as y }; //# sourceMappingURL=lazy-C0Y_eCnB.d.mts.map