Skip to content

Refresh lifecycle

A reactive read's refresh keeps trying until the server answers, for as long as the flow is collected, and each emission says how it stands in QuerySnapshot.refresh. Waiting for the network or for the session reads as waiting, never as a failure. The sample shows it on the Sync tab:

From the samples: LibraryMappings.kt · SyncView.swift

/** This snapshot in the app's terms, its rows decoded by [rows] when there are any. */
internal fun <T> QuerySnapshot.toRowsSnapshot(rows: (PostgrestResult) -> T): RowsSnapshot<T> =
    RowsSnapshot(
        value = result?.let(rows),
        isFromCache = isFromCache,
        fetchedAt = fetchedAt,
        hasPendingMutations = hasPendingMutations,
        refresh = refresh.toTaskRefreshState(),
    )

/** Where the refresh behind a live read stands, one case for each of the library's four. */
internal fun RefreshState.toTaskRefreshState(): TaskRefreshState =
    when (this) {
        RefreshState.Idle -> TaskRefreshState.Idle // the last refresh succeeded
        RefreshState.Refreshing -> TaskRefreshState.Refreshing // asking, or about to again
        RefreshState.Paused -> TaskRefreshState.Paused // the network is known to be down
        is RefreshState.Stalled -> TaskRefreshState.Stalled(cause) // failing while online
    }
/// The task list's refresh, in words.
private var refreshText: String {
    guard app.session.userID != nil else { return "Waits for the first sign-in" }
    switch app.tasks.refresh {
    case .idle: return "Up to date with the server"
    case .refreshing: return "Asking the server…"
    case .paused: return "Paused while offline"
    case .stalled: return "Can’t reach the server — showing saved data"
    }
}

The four states

State Meaning What to show
Idle No refresh is running or waiting: the last one succeeded, or the read does not refresh Nothing
Refreshing A refresh is running, is about to be tried again soon after a failure, or waits for a session that is still settling A quiet progress mark, if anything
Paused The refresh waits for the network, which the library knows to be unavailable. Nothing is failing "Offline": the rows are the device's
Stalled(cause) The refresh keeps failing while the network is believed to be up. It is still tried, at most a minute apart "Can't reach the server": the rows are the device's
stateDiagram-v2
    [*] --> Refreshing
    Refreshing --> Idle: the server answers
    Refreshing --> Stalled: the fourth failure in a row
    Refreshing --> Paused: it fails while the network is known down
    Paused --> Refreshing: the network comes back
    Stalled --> Idle: a later attempt succeeds
    Stalled --> Paused: it fails while the network is known down
    Idle --> Refreshing: a clear, a new owner, or a write marks the rows stale

Stalled.cause is the failure that stalled the refresh; a later, different failure does not replace it. It is supabase-kt's own exception, whose message carries the request's URL and its filter values: show your own words (Security and privacy).

The retry schedule

A failed refresh is tried again after 300 ms, 900 ms and 2.4 s, then after 5 s, 15 s and every 60 s. It reads Stalled after the fourth failure in a row, about 3.6 s in, and Paused instead while the network is known to be down. The wait is cut short, with an attempt at once, when the network comes back (the connectivity monitor or your networkAvailability turns online, or enableNetwork() is called) and when the session settles (a stored session finishes restoring, or a user signs in).

A refresh turns supabase-kt's own retries of a failed GET (four attempts over about 7 s by default) off for transport failures, so a dead network is noticed at once; a 503 or 520 is still retried on supabase-kt's schedule.

Cold start and session restore

Until supabase-kt's Auth plugin has restored the stored session, the refresh reads Refreshing, with the cached rows on screen (Signing out and switching users).

Under ResponseTrust.AuthenticatedSessionOnly, a live read of an empty table made signed out stays Refreshing, retried at most a minute apart, until the table has rows or a user signs in (without Auth, only rows end it): under that rule an empty answer is written only for a signed-in user.

An old answer never replaces a newer write

Every request reads a tick from a monotonic clock before it goes out, and every write that changes the cache moves its table's mark forward. An answer whose request left before the table's last change is refused, since it cannot hold that change, and the refresh asks again. One that left before clearLocalData() is refused and not asked again, since it may carry the cleared session's token; one that left before the cache passed to a new owner is not written, and the refresh asks again.