How does a Qt client cache stay fresh?

Table of Contents

Summary

A Qt-client-side cache (ChangeReasonCache, BadgeCache, both in ores.qt/api) stays fresh by subscribing, via ClientManager, to the NATS change-notification subject(s) for the entity/entities it mirrors, and reloading itself whenever a matching notification arrives – re-subscribing on every loggedIn=/=reconnected signal, since a fresh connection has no standing subscriptions. This is a distinct mechanism from the server-side entity-mirror cache (partitioned_cache + the nats-event-cache codegen facet): same idea – load, then reload on a change event – but a different tier, concurrency model, and transport, so the server-side facet is not the tool to reach for when a Qt cache goes stale. There is currently no codegen facet for the Qt-side pattern; it is hand-written per cache, following ChangeReasonCache as the reference implementation.

Detail

The pattern

  1. Construction: if clientManager_ is already logged in, subscribe immediately; otherwise wait for it.
  2. Connect ClientManager::loggedIn and ClientManager::reconnected to a subscribeToEvents() method that calls clientManager_->subscribeToEvent(event_name) for every change-event subject the cache cares about. Both signals matter – a reconnect drops and re-establishes NATS subscriptions server-side, so the client must re-subscribe every time, not just once at first login.
  3. Connect ClientManager::notificationReceived to a handler that compares the incoming eventType against the subject(s) from step 2, and on a match: mark the cache stale (is_loaded_ = false), call the existing loadAll(), and connect a one-shot (Qt::SingleShotConnection) lambda on the cache's own loaded signal to emit a refreshed signal once the reload actually lands – so callers that only care about "did something change" (as opposed to "trigger a load") have a signal to hook, without duplicating the load-completion bookkeeping onDefinitionsLoaded /=onMappingsLoaded= already do.
  4. Views reading the cache (e.g. EntityItemDelegate's badge resolvers) already call resolve()=/=fallback() at paint time, not once at setup time, so a repaint after loaded() fires is enough to pick up the fresh data – no separate invalidation path needed on the view side.

BadgeCache additionally demonstrates the case where the cache spans two backing message types (badge_definition and its badge_mapping join) but only one of them publishes a change-event – badge_mapping is a junction row with no generated entity handler of its own (see the dq messaging registrar), so editing a mapping does not, today, trigger a live refresh anywhere; only badge_definition edits (e.g. a colour change through BadgeDefinitionDetailDialog) do. A cache spanning a similar event/non-event split elsewhere should subscribe to whichever half does publish and accept that the other half needs a reconnect to pick up out-of-band changes (e.g. a direct SQL edit in a test/ops scenario), rather than inventing a mapping-level event for a rarely- edited join table.

Why not the server-side entity-mirror cache instead

The server-side entity-mirror cache (partitioned_cache<PartitionKey,Key,Value> + the nats-event-cache facet) solves the same-shaped problem – load, then reload on a change event – but at a different tier, and adopting it for a Qt cache would import machinery built for problems the GUI tier doesn't have while leaving the one it does have (interactive login/logout) unmodelled:

Dimension Server-side entity-mirror cache Qt-client cache (this pattern)
Tier Backend service caching another service's data GUI presentation cache, one process
Concurrency Multi-threaded NATS handlers, no event loop -- Single-threaded Qt event loop; all mutation
  needs lock-free immer::atom<immer::map<...>> already serialised via =QFutureWatcher=/
    signal-slot on the GUI thread
Auth Service-account JWT via a token_provider The logged-in human user's session, via
  (make_service_token_provider); no login concept ClientManager
Partitioning Keyed by tenant_id, warmed for every tenant the One active tenant per session – nothing to
  service might ever be asked about partition
Protocol A dedicated generated read_{entity}_for_cache_* Reuses the entity's existing request (e.g.
  request/response pair, separate from normal CRUD get_badge_definitions_request) the Qt
    client already calls
Lifecycle Warmed once at service startup; lives for the Must react to login/logout/reconnect of one
  process's lifetime; no reconnect concept human operator

Reaching for the facet here would mean generating a redundant read_badge_definitions_for_cache handler duplicating an existing message, pulling ores.eventing.core=/=immer into the Qt build (a layering boundary the GUI tier doesn't cross today), and building service-token plumbing for a client that already has a real user session – solving problems that don't exist at this tier while leaving the one that does (session lifecycle) unaddressed.

See also

Emacs 29.3 (Org mode 9.6.15)