# Infinite Queries

use_infinite_query API reference for gpui-query: bidirectional page fetching, max_pages limits, and rendering paginated data in GPUI components.

Infinite queries handle paginated data: feed streams, cursor-based lists, any scenario where you load a chunk at a time and might need more later. The API is modeled after TanStack Query's `useInfiniteQuery`, adapted to GPUI's synchronous rendering model and Rust's ownership semantics.

## use_infinite_query

```rust
use gpui_query::{use_infinite_query, InfiniteQueryOptions, QueryKey};
use gpui::{Entity, Subscription};

let (entity, subscription) = use_infinite_query(
    InfiniteQueryOptions::new(QueryKey::from(["feed"])),
    |last_page| async move {
        let cursor = last_page.map(|p| p.cursor());
        let page = fetch_page(cursor).await?;
        Ok::<_, MyError>((page.items, page.has_more))
    },
    cx,
);
```

The hook returns two things: an `Entity<InfiniteQueryResource<T, E>>` holding all loaded pages, and a `Subscription` that keeps the observation alive. Drop the subscription and the view stops re-rendering on state changes.

The fetcher closure receives `Option<&T>` (the last page for forward fetches, the first page for backward fetches, or `None` on the initial request). It returns `Result<(T, bool), E>` where the `bool` tells the query whether more pages exist.

On creation, the hook starts an initial fetch if the resource is idle. The entity is registered with [QueryClient](/docs/api/query-client) for shared caching and garbage collection.

### Minimal example

```rust
struct FeedView {
    feed: Entity<InfiniteQueryResource<Vec<Post>, MyError>>,
    _subscription: Subscription,
}

impl FeedView {
    fn new(cx: &mut Context<Self>) -> Self {
        let (entity, sub) = use_infinite_query(
            InfiniteQueryOptions::new(QueryKey::from(["feed"])),
            |last_page| async move {
                let cursor = last_page.and_then(|p| p.last().map(|post| post.id));
                let resp = api::fetch_feed(cursor).await?;
                Ok((resp.items, resp.has_next))
            },
            cx,
        );
        Self { feed: entity, _subscription: sub }
    }
}
```

## fetch_next_page_infinite

Call this from event handlers to load the next page. Common trigger: the user scrolled to the bottom of a list.

```rust
use gpui_query::fetch_next_page_infinite;

fn on_scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
    fetch_next_page_infinite(
        &self.feed,
        |last_page| async move {
            let cursor = last_page.and_then(|p| p.last().map(|post| post.id));
            let resp = api::fetch_feed(cursor).await?;
            Ok((resp.items, resp.has_next))
        },
        cx,
    );
}
```

The function reads the last page from the entity and passes it to your fetcher. It returns nothing; the entity updates in place and the view re-renders when the fetch completes.

If `has_next_page()` is `false`, the call does nothing. If a fetch is already in flight under `RequestPolicy::LatestWins`, the old request is cancelled and replaced.

## fetch_previous_page_infinite

Same shape as `fetch_next_page_infinite`, but fetches in the backward direction. The fetcher receives the first page (not the last) so you can compute a cursor for older content.

```rust
use gpui_query::fetch_previous_page_infinite;

fn on_scroll_to_top(&mut self, cx: &mut Context<Self>) {
    fetch_previous_page_infinite(
        &self.feed,
        |first_page| async move {
            let cursor = first_page.and_then(|p| p.first().map(|post| post.id));
            let resp = api::fetch_feed_before(cursor).await?;
            Ok((resp.items, resp.has_prev))
        },
        cx,
    );
}
```

This is useful for chat interfaces or timelines that grow in both directions. Set `FetchDirection::Bidirectional` on the resource so `has_previous_page` defaults correctly.

## InfiniteQueryResource\

The core state container. It holds pages in a `VecDeque<T>` and tracks fetch status, error state, and direction flags.

### Construction

```rust
// Forward-only (default). has_next_page starts true.
let mut resource = InfiniteQueryResource::new(
    QueryKey::from("items"),
    CachePolicy::Ttl { ttl_ms: 60_000 },
    RequestPolicy::LatestWins,
);

// Bidirectional. Both has_next_page and has_previous_page start false.
let mut resource = InfiniteQueryResource::new_bidirectional(
    QueryKey::from("items"),
    CachePolicy::Ttl { ttl_ms: 60_000 },
    RequestPolicy::LatestWins,
);
```

Use `new_bidirectional` when you need to prepend pages. The default constructor assumes forward-only pagination.

### Page accessors

| Method | Returns | Notes |
|--------|---------|-------|
| `pages()` | `&VecDeque<T>` | All loaded pages, first to last |
| `page_count()` | `usize` | Number of loaded pages |
| `first_page()` | `Option<&T>` | First loaded page |
| `last_page()` | `Option<&T>` | Last loaded page |
| `has_next_page()` | `bool` | Whether more pages exist after the last |
| `has_previous_page()` | `bool` | Whether more pages exist before the first |
| `has_data()` | `bool` | True when at least one page is loaded |
| `is_page_data_valid()` | `bool` | True when loaded pages are safe to use |

`is_page_data_valid()` deserves a note. It returns `true` on `Success`, on `LoadingWithData` (previous pages still valid while fetching more), and on `Failure` if pages were already loaded. A page fetch failure does not wipe existing pages. It returns `false` on `Idle` and `Cancelled`.

### Fetch state

| Method | Returns | Notes |
|--------|---------|-------|
| `is_fetching_next_page()` | `bool` | Next-page fetch in flight |
| `is_fetching_previous_page()` | `bool` | Previous-page fetch in flight |
| `is_loading()` | `bool` | Any fetch in flight |
| `status()` | `QueryStatus` | Current lifecycle status |
| `error()` | `Option<&E>` | Most recent error |

### Lifecycle methods

**begin_fetch_next()** and **begin_fetch_previous()** start a page fetch. They return `Option<RequestId>` (or `None` if the direction flag is `false` or a fetch is already in flight under `IgnoreWhileLoading`). Under `LatestWins`, an existing fetch is cancelled and replaced.

The `_with_id` variants accept a pre-allocated `RequestId` from the `QueryClient` bucket's sequencer to maintain monotonic IDs across the resource's lifetime. The `_auto` variants use an internal sequencer stored on the resource.

**complete_page_success(request_id, page, has_more, is_next, now_ms)** finishes a successful fetch. If the request ID matches the active request, the page is appended (next) or prepended (previous). Returns `true` if accepted, `false` if stale (the request was replaced). Passing `is_next = true` appends; `is_next = false` prepends.

**complete_page_failure(request_id, error)** records a failure. Existing pages are preserved.

```rust
let mut seq = RequestSequencer::new();
let id = resource.begin_fetch_next(&mut seq, now_ms);

// Later, when the async fetch completes:
let accepted = resource.complete_page_success(id, page_data, true, true, now_ms);
if !accepted {
    // Request was replaced by a newer one. Discard this result.
}
```

### Two-phase completion

For cases where you need to inspect or transform data between validation and completion:

```rust
// Phase 1: Accept the request, getting a guard
let guard = resource.accept_current_request(request_id);

if let Some(guard) = guard {
    // Phase 2: Use the guard to complete
    resource.complete_success_with_guard(&guard, page_data, has_more, is_next, now_ms);
    // Or on failure:
    // resource.complete_failure_with_guard(&guard, error);
}
```

The guard proves the request was still current at acceptance time. This is the same pattern used by [Queries](/docs/api/queries) on `QueryResource`.

### Page management

**append_page(page)** adds a page to the end. **prepend_page(page)** adds to the front. Both return `Vec<T>` of evicted pages when `max_pages` is exceeded.

**set_max_pages(max)** changes the cap. Returns evicted pages immediately if the current count exceeds the new limit. A value of `Some(0)` is treated as unbounded (`None`) to prevent accidentally draining all pages.

**set_has_next_page(bool)** and **set_has_previous_page(bool)** set the direction flags manually.

### Reset and invalidate

**reset()** clears all pages, errors, and fetch state. The resource returns to `Idle`. The `max_pages` setting is preserved. The `has_next_page` flag resets according to `FetchDirection`: `true` for `ForwardOnly`, `false` for `Bidirectional`.

**invalidate()** clears the `last_updated_at` timestamp without touching pages. The data stays in memory but is considered stale, so the next access triggers a refetch.

## FetchDirection

```rust
pub enum FetchDirection {
    ForwardOnly,    // default
    Bidirectional,
}
```

Controls the default assumptions for `has_next_page` and `has_previous_page` after construction and after `reset()`.

`ForwardOnly` (the default) sets `has_next_page` to `true` and `has_previous_page` to `false`. This is the right choice for feed-style pagination where you only append.

`Bidirectional` sets both to `false`. The query will not fetch in either direction until you explicitly call `set_has_next_page(true)` or `set_has_previous_page(true)`, or until a successful fetch returns `has_more = true`.

## InfiniteQueryOptions

Builder for configuring the hook. All methods return `Self` for chaining.

```rust
let opts = InfiniteQueryOptions::new(QueryKey::from(["feed"]))
    .max_pages(10)
    .cache_policy(CachePolicy::Ttl { ttl_ms: 300_000 })
    .request_policy(RequestPolicy::LatestWins)
    .retry_policy(RetryPolicy::new(3))
    .gc_time(600_000);
```

| Field | Default | Description |
|-------|---------|-------------|
| `key` | (required) | Cache key via `QueryKey` |
| `max_pages` | `Some(50)` | Max pages retained. Oldest evicted on overflow |
| `cache_policy` | `CachePolicy::default()` | TTL, stale-while-revalidate, or no cache |
| `request_policy` | `RequestPolicy::default()` | `LatestWins` or `IgnoreWhileLoading` |
| `retry_policy` | `RetryPolicy::default()` | 3 retries with exponential backoff |
| `gc_time_ms` | `300_000` | Garbage collection time in milliseconds |

### max_pages vs unbounded_pages

```rust
// Cap at 20 pages
InfiniteQueryOptions::new(key).max_pages(20)

// No limit (use with caution)
InfiniteQueryOptions::new(key).unbounded_pages()
```

The default cap is 50 pages. This prevents unbounded memory growth when a user scrolls through thousands of items. When the cap is exceeded, the oldest pages are evicted. For forward pagination, pages drop from the front. For backward pagination, pages drop from the back.

Call `unbounded_pages()` only if you have a concrete reason to keep all pages in memory and understand the tradeoff.

## Request IDs and staleness

Every fetch gets a unique `RequestId` from a monotonically increasing sequencer. The completion methods (`complete_page_success`, `complete_page_failure`) check whether the ID still matches the active request. If it does not match, the result is discarded and `ignored_results` is incremented.

This matters under `RequestPolicy::LatestWins`. When a user scrolls fast and triggers multiple fetches, each new fetch cancels the previous one. Late-arriving responses from cancelled fetches are silently dropped.

## Rendering state

A typical render method reads the entity and branches on status:

```rust
impl Render for FeedView {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
        self.feed.read_with(cx, |resource, _| {
            match resource.status() {
                QueryStatus::LoadingEmpty => "Loading first page...",
                QueryStatus::LoadingWithData => {
                    // Show existing pages with a loading indicator at the bottom
                    render_pages(resource.pages())
                }
                QueryStatus::Success => render_pages(resource.pages()),
                QueryStatus::Failure => {
                    if resource.is_page_data_valid() {
                        // Previous pages still visible, show error alongside them
                        render_pages_with_error(resource.pages(), resource.error())
                    } else {
                        "Failed to load"
                    }
                }
                _ => "Idle",
            }
        })
    }
}
```

The key detail: `Failure` does not mean "no data." It means the last page fetch failed. Previously loaded pages remain accessible and valid. Check `is_page_data_valid()` before deciding what to show.
