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
- Construction: if
clientManager_is already logged in, subscribe immediately; otherwise wait for it. - Connect
ClientManager::loggedInandClientManager::reconnectedto asubscribeToEvents()method that callsclientManager_->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. - Connect
ClientManager::notificationReceivedto a handler that compares the incomingeventTypeagainst the subject(s) from step 2, and on a match: mark the cache stale (is_loaded_ = false), call the existingloadAll(), and connect a one-shot (Qt::SingleShotConnection) lambda on the cache's ownloadedsignal to emit arefreshedsignal 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 bookkeepingonDefinitionsLoaded/=onMappingsLoaded= already do. - Views reading the cache (e.g.
EntityItemDelegate's badge resolvers) already callresolve()=/=fallback()at paint time, not once at setup time, so a repaint afterloaded()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
- Where should an entity-mirror cache live? – the server-side counterpart this pattern is contrasted with.
- Generic entity-mirror
cache + codegen facet – the story that built
partitioned_cacheand thenats-event-cachefacet.