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.