Package-level declarations

Types

Link copied to clipboard

Connectivity as the decision core sees it. CaptivePortal is "linked but walled off".

Link copied to clipboard

The heart of the offline-first read path: a PURE function from (data snapshot, connectivity) to the single ScreenState a screen renders. No coroutines, no I/O — exhaustively unit-testable.

Link copied to clipboard

Coarse error classification the DecisionEngine routes on. The caller supplies the classifier.

Link copied to clipboard
sealed interface FetchPolicy

Whether a screen reads from cache, the network, or both — the read strategy a caller hands to its store stream. Kept as a sealed interface so new variants can be added without breaking the FetchPolicy.NetworkWithCache access pattern.

Link copied to clipboard

How stale cached data is, on a time axis only — network state is deliberately NOT an input (that's a separate "am I online" concern). Drives a per-card freshness indicator ("Updated 2 min ago" → "Refresh now" → "Retry").

Link copied to clipboard
sealed interface MutationState<out R>

The write-side counterpart to ScreenState: the state of a single mutation (submit/update/delete) a screen renders while it runs. One exhaustive type instead of isSubmitting + submitError flags.

Link copied to clipboard
sealed interface ScreenState<out T>

The one UI state an offline-first screen renders. Collapses the usual scatter of isLoading / isFromCache / isRefreshing / error / networkStatus flags into a single exhaustive type a when can render. Produced by DecisionEngine.decide from a StoreData snapshot + Connectivity.

Link copied to clipboard
data class ScreenWithMutation<T, R>(val screen: ScreenState<T>, val mutation: MutationState<R> = MutationState.Idle, val outboxPending: Int = 0, val isSyncing: Boolean = false)

The whole state of a screen that both DISPLAYS data and MUTATES it — the read ScreenState and the write MutationState in one snapshot, plus the offline-sync status (outboxPending count + isSyncing) so a "3 pending · syncing…" banner needs no extra plumbing.

Link copied to clipboard
data class StoreData<out T>(val data: T?, val error: Throwable? = null, val fetchedAt: Instant? = null, val isRefreshing: Boolean = false)

A snapshot from the caller's data pipeline: the latest data (or null), the last error, when it was last successfully fetched (fetchedAt), and whether a refresh is in flight. Deliberately not tied to any store library — a repository fills this in however it likes.

Functions

Link copied to clipboard
fun freshnessBand(now: Instant, lastSyncedAt: Instant?, ttl: Duration, lastError: Throwable? = null): FreshnessBand

Pure staleness computation. First match wins:

Link copied to clipboard
fun <T> freshnessStream(storeData: Flow<StoreData<T>>, ttl: Duration, now: () -> Instant = { Clock.System.now() }): Flow<FreshnessBand>

Sibling stream: the per-card FreshnessBand over time, computed purely from (fetchedAt, ttl, lastError) — network state is deliberately NOT an input (a separate connectivity banner owns that). Run alongside screenStateStream to drive a freshness indicator that updates independently of the content decision.

Link copied to clipboard
fun <T> screenStateStream(storeData: Flow<StoreData<T>>, connectivity: Flow<Connectivity>, ttl: Duration, now: () -> Instant = { Clock.System.now() }, classify: (Throwable) -> ErrorKind = { ErrorKind.Other }): Flow<ScreenState<T>>

The reactive read path: turns a data Flow + a connectivity Flow into a Flow<ScreenState<T>> by running DecisionEngine.decide on each emission. This is the live form of the pure decision core — a ViewModel collects this and exposes it as UI state.

Link copied to clipboard
fun <T, R> screenWithMutationStream(screen: Flow<ScreenState<T>>, mutation: Flow<MutationState<R>> = flowOf(MutationState.Idle), outboxPending: Flow<Int> = flowOf(0), isSyncing: Flow<Boolean> = flowOf(false)): Flow<ScreenWithMutation<T, R>>

Combines the read state, the write state, and the offline-sync signals into one stream. Only screen is required; the rest default to a single benign emission, so the combined stream emits as soon as the screen does. The app wires outboxPending/isSyncing to its :offline-outbox queue if it uses one.

Link copied to clipboard
fun <P, R> submitFlow(payload: P, mutation: suspend (P) -> R, isRetryable: (Throwable) -> Boolean = { false }, enqueueOffline: suspend (P) -> Unit? = null): Flow<MutationState<R>>

The offline-first write path: runs mutation and emits MutationState (Submitting → Success/Failed). On a failure classified retryable by isRetryable (offline / 5xx / timeout), it hands payload to enqueueOffline so the write is durably queued for later replay — then reports Failed(queuedOffline = true), letting the UI say "saved, will sync". Non-retryable failures (a 4xx the server rejected) surface as Failed(queuedOffline = false).