MutationQueue

class MutationQueue(apiClient: ApiClient, scope: CoroutineScope, store: MutationStore = InMemoryMutationStore(), defaultRetryPolicy: RetryPolicy = DefaultRetryPolicy(retryNonIdempotent = true), codecs: List<MutationCodec<*>> = emptyList())

Enqueues "forgivable" mutations (likes, follows, and similar fire-and-forget writes) for background execution with retry, instead of awaiting them inline from a ViewModel.

enqueueMutation/enqueue return immediately — the actual ApiClient.request call and any retries run on scope, which the caller owns (e.g. an app-scoped CoroutineScope that outlives any single screen). This is the fix for two problems with calling apiClient.request(...) directly for mutations:

  1. No queueing: if the process dies mid-flight, the mutation is silently lost.

  2. No retry: DefaultRetryPolicy deliberately excludes POST/PATCH by default (retrying a non-idempotent call risks a duplicate write), so a plain NetworkClient never retries them — correct for most calls, wrong for ones the caller has decided are safe/idempotent-in-effect to retry (e.g. "set like state to true" is idempotent even though it's a POST).

Behavior:

Parameters

apiClient

executes the underlying HTTP calls.

scope

owns every mutation's execution + retry backoff; mutations outlive the caller's own scope (e.g. a ViewModel's) as long as this scope is alive.

store

where enqueued mutations (that were given a MutationCodec) are persisted.

defaultRetryPolicy

applied to mutations enqueued without an explicit retryPolicy. Defaults to retrying non-idempotent methods too — the whole point of this queue is to make POST/PATCH mutations retryable — unlike DefaultRetryPolicy's own conservative default.

codecs

MutationCodecs to register upfront, so restore can decode their records even before any matching enqueue call runs in this process. Enqueueing with a new codec also registers it.

Constructors

Link copied to clipboard
constructor(apiClient: ApiClient, scope: CoroutineScope, store: MutationStore = InMemoryMutationStore(), defaultRetryPolicy: RetryPolicy = DefaultRetryPolicy(retryNonIdempotent = true), codecs: List<MutationCodec<*>> = emptyList())

Functions

Link copied to clipboard
suspend fun MutationQueue.enqueue(key: MutationKey, endpoint: NetworkEndpoint, retryPolicy: RetryPolicy? = null): MutationHandle

Enqueues a bodyless mutation (e.g. POST /videos/42/like with no request body).

inline suspend fun <B : Any> MutationQueue.enqueue(key: MutationKey, endpoint: NetworkEndpoint, body: B, codec: MutationCodec<B>? = null, retryPolicy: RetryPolicy? = null): MutationHandle

Enqueues a mutation sending body of type B, serialized on execution.

Link copied to clipboard
suspend fun <B : Any> enqueueMutation(key: MutationKey, endpoint: NetworkEndpoint, body: B? = null, bodyType: TypeInfo? = null, codec: MutationCodec<B>? = null, retryPolicy: RetryPolicy? = null): MutationHandle

Enqueues a mutation and returns immediately; endpoint (with body, described by bodyType) is executed on scope in the background, retried per retryPolicy (or defaultRetryPolicy when null), and coalesced with any other pending mutation sharing key.

Link copied to clipboard
suspend fun restore()

Loads every persisted MutationRecord from store and resubmits it for execution, using the MutationCodec registered under MutationRecord.codecId (via the constructor's codecs or a prior enqueue call) to reconstruct the endpoint + body.

Link copied to clipboard
fun statusFlow(key: MutationKey): StateFlow<MutationStatus?>

The current/most recent MutationStatus for key, or null if nothing has ever been enqueued under it. Keeps emitting past a terminal MutationStatus.Succeeded/MutationStatus.Failed until a new mutation is enqueued for the same key.