Skip to content
In development. Nothing in Store 6 is published yet.

Pending-write UI

Two affordances that look alike and are not

When the projector returns a different non-null value, Store emits a StoreResult.Data frame with origin = Origin.OVERLAY, age = Duration.ZERO, and isStale = false. That freshness stamp is unconditional. The optimistic value is new because the user just wrote it. Only refreshing remains live on an overlay frame, reflecting whether a fetch is in flight for the key.

Keep the two badges independent in UI code:

kotlin
import androidx.compose.runtime.Composable
import org.mobilenativefoundation.store6.core.Origin
import org.mobilenativefoundation.store6.core.StoreResult

@Composable
fun <V> WriteBadges(
    result: StoreResult<V>,
    saving: @Composable () -> Unit,
    stale: @Composable () -> Unit,
) {
    when (result) {
        is StoreResult.Data -> {
            if (result.origin == Origin.OVERLAY) saving()
            if (result.isStale) stale()
        }
        else -> Unit
    }
}

A spinner driven by isStale will never appear for a pending optimistic frame. If the projector returns a value equal to the committed base, Store preserves the committed envelope and its origin instead of labelling it OVERLAY. Use durable inspection when the UI needs pending counts or phases independent of whether projection changed the visible value.

get never sees pending writes

The overlay applies only to stream. MutationStore.get resolves the terminal canonical key and then performs an unprojected point read of committed truth. An optimistic mutation is therefore invisible to get, even while stream can show its projection.

Observe stream when the screen must see its own optimistic write. The read contract explains why Store keeps the point-read and observation doors distinct.

Narrating the OVERLAY → SOT flip

When Store durably records an acknowledgement as ACKED, it will not push that generation again. It then adopts the server echo as committed truth, applies invalidation effects, and retires the intent. After those local steps finish, that intent no longer contributes an optimistic projection. The visible transition is:

MomentWhat the UI may observe
Pending projectionData(origin = OVERLAY, age = Duration.ZERO, isStale = false, refreshing = <live fetch state>)
Durable acknowledgement and local completionThe server echo is adopted, then the optimistic intent is retired.
A stream opened after the drain completesThe committed echo as Data attributed to SOT or MEMORY.

The origin distinguishes “saving” from “saved.” isStale never represents the pending phase, and refreshing remains a separate report of the live fetch slot.

Remote acceptance is earlier than durable acknowledgement. If Store fails or dies before the local acknowledgement-receipt transaction commits, the last durable phase remains INFLIGHT, and a later explicit drain may replay the same immutable generation and idempotencyKey. Replay after process death requires journal storage that survives restart. Once ACKED is durable, recovery may repeat adoption, effects, or retirement, but never the push. Keep the endpoint idempotent. The server guide defines that contract.

Optimistic creates and deletes on screen

Projection is defined over both a committed value and committed absence:

Committed baseProjection resultStream presentation
Absent (null)Non-nullOverlay data: an optimistic create.
Non-nullnullThe normal absent/loading transition: an optimistic delete.

An optimistic create can render the new item immediately with origin = OVERLAY. An optimistic delete looks like removal through the normal absence path, not like a mutation error.

Richer affordances

Use durable inspection for queue counts, per-intent phases, and settlement state:

APISnapshot
pending(key)Pending intents for the key's terminal identity in durable client-sequence FIFO order.
pendingWrites()Every nonterminal active intent across durable identities in durable client-sequence order. Retired history is excluded.

The five public pending states are the complete mapping of nonterminal execution phases:

Public stateNonterminal execution phase
PENDINGUNPREPARED or READY
INFLIGHTINFLIGHT
REFRESHINGREFRESH_REQUIRED
ADOPTINGACKED
APPLYING_EFFECTSEFFECTS_PENDING

events is useful for transient presentation such as toasts or timeline chips, but it is advisory and lossy. Its shared flow has replay 0, extra buffer capacity 64, and DROP_OLDEST overflow. Events can disappear under pressure, new collectors receive no history, and restart replays no completed events. Never use event delivery as drain, acknowledgement, retry, or settlement truth. Re-read durable inspection instead. The inspection guide covers pending state, dead letters, and advisory telemetry together.

In Compose

The Compose adapter's collectAsState, lifecycle-aware collectors, and storeResultMutationPolicy() compare Data structurally. Value, origin, isStale, and refreshing all participate. age does not. An OVERLAYSOT or MEMORY origin change is therefore not equivalent, even when the value is equal, so an origin-keyed affordance recomposes on the observed flip.

See store6-compose for collection and recomposition details.


Source recorded 2026-08-12 ·main@539614c0· pre-6.0.0-alpha01