PaginatedCache<T> class

Infinite-list cache over a Syncache: fetches pages one at a time, accumulates them into a windowed list stored as a normal cached value under key, and exposes it as reactive watch state.

Because the window is a plain cached List<T>, everything the cache already does applies to it: persistence via any Store, optimistic mutations via SyncacheListExtension.mutateList / ListOperation (durable through a MutationOutbox), and invalidation by pattern or tag — invalidatePattern('posts?*') reaches the window key like any other.

Semantics

  • loadMore appends the next page. Page-fetch failures keep the window and the cursor unchanged (safe to retry) and surface via PagedViewState.error — the returned future never throws. Concurrent calls coalesce into one fetch.
  • refresh re-fetches the first page and merges: fresh items win at the front, duplicates (by ==) are removed from the retained tail.
  • External invalidation of key (pattern, tag, or direct) drops the tail and re-fetches page 1 when autoRefreshOnInvalidation is true (default). Eviction and clear reset the paging state but never auto-refetch (capacity events cause no refill storms).
  • maxItems bounds the window by dropping from the front; the server cursor is unaffected.
  • The page cursor is in-memory. After a process restart the stored window rehydrates, the cursor resets to firstPageKey, and a loadMore on a non-empty window refresh-merges instead of appending — so it can never duplicate the rehydrated items.

Mutating the window

Do not go through PaginatedCache — mutate the underlying cache directly; the state stream follows automatically:

final posts = PaginatedCache<Post>(
  cache: listCache, // Syncache<List<Post>>
  key: 'posts?feed=home',
  fetchPage: (pageKey) async {
    final response = await api.fetchPosts(cursor: pageKey as String?);
    return PageResult(response.items, nextPageKey: response.nextCursor);
  },
  maxItems: 500,
);

await posts.loadMore();

// Optimistic append, durable via the configured outbox:
await listCache.mutateList(
  key: 'posts?feed=home',
  operation: ListOperation.append(newPost),
  send: () => api.createPost(newPost),
  // Invalidate ITEM keys, never the window itself — a mutation that
  // invalidates its own window key drops the tail on every sync.
  invalidates: ['post:*'],
);

One PaginatedCache per cache key: two instances over the same key would both fetch and fight over the window. For scoped caches, pass the underlying Syncache and the already-prefixed key (cache.scoped('s').cache / 's:posts').

Constructors

PaginatedCache({required Syncache<List<T>> cache, required String key, required Future<PageResult<T>> fetchPage(Object? pageKey), Object? firstPageKey, int? maxItems, bool autoRefreshOnInvalidation = true})
Creates a paginated cache over cache's window at key.

Properties

firstPageKey → Object?
The key of the very first page (opaque; often null for cursor APIs).
final
hashCode → int
The hash code for this object.
no setterinherited
key → String
final
maxItems → int?
Maximum number of items kept in the window. When exceeded, items are dropped from the front (the oldest pages). null = unbounded.
final
runtimeType → Type
A representation of the runtime type of the object.
no setterinherited
state → PagedViewState<T>
The current window and fetch state.
no setter
watch → Stream<PagedViewState<T>>
Emits a new PagedViewState whenever the window or fetch state changes. Seeded: every listener first receives the current state. A stream over a disposed instance completes immediately.
no setter

Methods

dispose() → void
Detaches from the cache. The stored window survives; in-flight fetch results are discarded; the watch stream closes.
invalidate() → Future<void>
Drops the window and re-fetches the first page.
loadMore() → Future<void>
Fetches the next page and appends it to the window.
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
refresh() → Future<void>
Re-fetches the first page and merges it into the window: fresh items win at the front, duplicates (by ==) are dropped from the retained tail. Use this for pull-to-refresh semantics — scroll position and tail survive.
toString() → String
A string representation of this object.
inherited

Operators

operator ==(Object other) → bool
The equality operator.
inherited