search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs

Repositories

Repository is an id-keyed cache of one remote resource's rows, Eloquent's identity map for a client with no query builder; RepositoryQuery is one ordered, filtered, paginated view over it.

Introduction

A list screen and a show screen for the same resource used to keep two separate copies of the same row, one per MagicStateMixin; a patch on one never reached the other. Repository holds one row per id, and every RepositoryQuery built over it reads that row live, so a patch or an evict shows up everywhere at once, with no refetch.

Writing a Repository

Subclass Repository with the three pieces that describe the resource:

class MonitorRepository extends Repository {
  static MonitorRepository instance = MonitorRepository();

  @override
  String get resource => 'monitors';

  @override
  Monitor Function(Map) get fromMap => Monitor.fromMap;
}

Never register a Repository via Magic.put/Magic.findOrPut: its constructor registers it with SessionScope on its own, which is how a session reset reaches it (see Session Resets). A plain static singleton, as above, is the right shape.

RepositoryQuery

RepositoryQuery owns the ordering, the cursor, and the filters over a repository; the rows themselves live in the repository. It reuses MagicPaginator.fetcher to walk pages, and writes every page it reads into the repository via Repository.upsertFromList.

final query = RepositoryQuery(
  repository: MonitorRepository.instance,
  perPage: 50,
  filters: {'status': statusFilter},
);

await query.ensureFresh(); // joins the first reload if one is already in flight
query.items;               // live rows, in server order
query.hasMore;
query.isLoadingMore;
await query.loadMore();
await query.setFilters({'status': 'down'}); // reloads from the first page

Built by the controller that owns it, one per screen; dispose it from that controller's onClose:

class MonitorListController extends MagicController {
  late final query = RepositoryQuery(repository: MonitorRepository.instance);

  @override
  void onClose() {
    query.dispose();
    super.onClose();
  }
}

showOnlyKeys

A list endpoint often does not measure every field a show endpoint does (uptime_24h, a computed aggregate). Override showOnlyKeys to name those fields: a null the list sends for one of them keeps whatever the cache already holds from a prior upsertFromShow, instead of erasing it.

class MonitorRepository extends Repository {
  @override
  Set get showOnlyKeys => const {'uptime_24h'};
}

A field outside showOnlyKeys that the list sends as null DOES become null: the list is the source of truth for everything it was not told to leave alone. upsertFromShow is authoritative for every field regardless, since the show endpoint measures them all.

Reading and Writing a Single Row

Method What it does
find(id) The cached row for id, or null
all Every cached row, in no particular order
refresh(id) Re-reads GET $resource/$id; a 404 evicts the row and answers null, any other failure hands back the cached copy
patch(id, attributes) Merges attributes onto the cached row; a no-op when nothing is cached for id
evict(id) Drops the cached row, if any

refresh's 404-evicts, other-failure-keeps split matters: a fault of ours (a timeout, a 500) is never a verdict about the row, so only a confirmed "this row is gone" clears the cache.

Every write path notifies its listeners (and so every RepositoryQuery over it) only when the cache actually changed: a row that was not cached, an evicted row, or an attribute whose stored value differs, compared deeply and with map keys in any order. A broadcast patch that restates what is cached, or a refresh that answers the row already held, rebuilds nothing. upsertFromShow still stores the answered instance, so find(id) is the row refresh returned. A controller that needs to repaint once a read settles (to clear its own loading flag) calls its own refreshUI() after the await; it cannot count on the repository's notify, which a failed read never sent either. A RepositoryQuery reload is unaffected: its page landing notifies through the paginator whether or not the rows changed.

Session Resets

A Repository implements SessionScoped and registers itself with SessionScope in its own constructor, so a login or a team switch drops every cached row before the owning controller refetches (see doc/digging-deeper/session-scope.md). A RepositoryQuery built over it registers its own reset hook through Repository.addResetHook, clearing its cursor, its filters' resolved ids, and its isFirstLoad/hasMore flags alongside the rows, so a screen mid-load when the identity changes shows its first-load skeleton for the new session rather than a stale page.

Failure Modes

Situation What happens
A list page throws mid-fetch RepositoryQuery.items and .meta keep what they already held; loadFailed is true only when items is also empty
A page lands after a session reset (repository.epoch moved) The page's rows never reach the repository or the query's ids
refresh(id) gets a 404 The row is evicted; answers null
refresh(id) gets a 500 or times out The cached row stands; answers the cached copy
patch(id, ...) called for an id nothing has fetched No-op: a partial patch never fabricates a row nobody fetched

Generating a Repository

dart run magic:artisan make:repository Monitor            # -> MonitorRepository
dart run magic:artisan make:repository MonitorRepository  # Suffix already present
dart run magic:artisan make:repository Monitor --force    # Overwrite existing file