Skip to content

Data Flow ​

This page traces how entity data moves between the backend, the cache, the list controller, and the workspace UI — and how mutations flow back the other way without manual refresh wiring. The unifying idea is mutation announcement + subscription: writers announce what changed, the cache service brokers the announcement against everyone who subscribed, and controllers re-read.

Read path ​

EntityListController<T> holds the query lifecycle (page, page size, search, sort, filters, drafts toggle) as raw MobX observables and uses cached_query_flutter for stale-while-revalidate streaming. The controller exposes a Stream<QueryStatus<List<T>>> that EntityView(query:) subscribes to. While the cache reissues a request in the background, EntityView keeps showing cached data and surfaces a LinearProgressIndicator.

EntityView<T> accepts four data-source modes: future, stream, data, and query. The query mode is the default for list / detail surfaces because it integrates with the cache's invalidation events.

EntityApi and HttpEntityApi ​

EntityApi<T> is the abstract CRUD contract:

  • getOne, getMany, create, update, delete, count, findPosition.
  • getDrafts, countDrafts for Draftable entities.
  • entityType, schemaType, defaultSortField, hasDrafts.

HttpEntityApi<T> implements it over REST. It composes:

  • An EndpointBuilder that constructs URIs (getMany, getOne(id), create, update(id), delete(id), count, findPosition(id) and custom paths via buildUri).
  • A CachingPolicy (staleDuration, excludedOperations) — override on your subclass to change behaviour, or use CachingPolicy.none to disable.
  • A CacheKeyBuilder that namespaces every key under entityType.

For non-CRUD endpoints (workflow transitions, custom actions), HttpEntityApi exposes typed helpers (post, put, patch, plus mutate(uri:, method:, body:, announces:)) that take a Set<Mutation> to announce on success. This is how a custom workflow API keeps the cache coherent without touching the read site.

The cache service ​

QueryCacheService is the central facade. It runs two cooperating caches:

  1. Lightweight one-shot cache — Map<String, _CacheEntry> plus in-flight deduplication. Used by HttpEntityApi.cachedGet.
  2. Reactive observable cache — backed by cached_query_flutter. Used by QueryCacheService.observe(...) to produce ObservableStream<QueryStatus<T>> for MobX widgets.

Both caches share a single subscription / announcement registry, so a mutation invalidates entries regardless of which path produced them.

Mutation announcement and subscription ​

Mutation carries (entityType, MutationType, entityId?). The three types are create, update, delete. A subscription with a non-null entityId matches only that id (or an entity-wide announcement). Mutation.any(type) matches any MutationType for the type.

HttpEntityApi.create / update / delete automatically fan out the right announcements. Custom write endpoints declare their announcements explicitly through the announces: parameter on post / put / patch.

InvalidationEvent stream ​

QueryCacheService.invalidations is a broadcast Stream<InvalidationEvent>. Every workspace template subscribes to it through WorkspaceTemplate's shared infrastructure and dispatches typed MutationEvents (EntityCreated, EntityUpdated, EntityDeleted) to the workspace's onAfterMutation callback. This is how the UI auto-refreshes after CRUD operations or SSE pushes — no manual refresh() calls needed.

EntityListController ​

EntityListController<T> owns four concerns: query lifecycle, query params, selection, and counts. It deliberately does not own detail data, tab state, or panel control — those belong in widgets.

Key state (raw Observables, never exposed directly):

  • Query: _currentQuery, _querySub, _queryStream, _isLoading, _entities.
  • Params: _currentPage, _pageSize, _searchQuery, _sortField, _sortAscending, _showDrafts, plus an EntityFilterState.
  • Selection: _selectedEntityId, _selectedEntityIds, _selectedEntityDetail, _enableMultiSelect.
  • Counts: _totalCount.

A baseFilter (always-applied, invisible) lets a workspace scope itself to a fixed slice (e.g. an Inbox tab restricted to a particular activity type) without polluting the user-facing filter UI. The controller also subscribes to QueryCacheService.invalidations and re-issues the current query when its entity type is invalidated.

Drafts and versioning ​

For entities with Draftable, EntityApi.getDrafts and countDrafts power the "Drafts" tab. EntityListController exposes a _showDrafts toggle that swaps the underlying call. Each entity comes back hydrated with a DraftMetadata payload (status, operation, workflow link, revision comments, canEditDraft); UI uses these to drive draft badges, banners, and edit gating.

The full lifecycle (draft → underReview → approved → effective, or rejected / cancelled / failed terminal states) is described on the drafts & versioning guide.

EntityVersion and EntityAudit records back the version-history and audit-trail tabs. Both expose user attribution; the EntityNameCacheService resolves display names client-side without per-row API calls.

Editor write path ​

A StandardEntityEditor<T> calls api.create or api.update directly. A SignatureDrivenEditor<T> first resolves a LifecycleRequirements value, and when verification is required it wraps the save inside the signature service so remarks flow back into the API call:

When approvals are configured for the operation, SignatureDrivenEditor short-circuits verification and saves to draft instead — verification happens later through the workflow system.

Realtime updates ​

EntityRealtimeService is the polling-based realtime channel registered by the plugin. Features that need push semantics can wire SSE or WebSocket sources to call QueryCacheService.invalidateEntity(entityType) or invalidateById(entityType, id). From there the existing subscription path takes over and everything that observes those keys re-fetches.

Error handling ​

HttpEntityApi surfaces failures as ApiResponseException (or richer typed errors when the backend returns an ApiErrorResponse envelope). The UI package's ErrorDisplayService resolves any error into a DisplayableError for consistent rendering. Always surface failures through ErrorResolver.resolve(e).message and showApiErrorDialog — never raw exception strings, and never SnackBar (the app forbids it).

Next steps ​