Skip to content

Swift: Sync

sync() pushes the outbox until it stops draining, drives queued attachment transfers when the client was created with an attachment root, then pulls until the checkpoint closes. It runs the engine call on a dedicated dispatch queue, so an iOS app can call it from the main actor without blocking a frame.

Examples#

Basic#

// TodoApp/TodoListView.swift (excerpt)
import KizunaSync

try await kizunasync.sync()

From the scheduler#

// TodoApp/TodoSync.swift (excerpt)
import KizunaSync
import Supabase

let syncScheduler = KizunaSyncScheduler(
  client: kizunasync,
  refreshSession: {
    guard let session = try? await supabase.auth.session else { return false }
    try? await kizunasync.setAccessToken(session.accessToken)
    return true
  }
)

A scheduler built with no sync closure calls this client's sync() on every run: when it starts, after every local write, on its timer, when the network comes back, and when the app returns to the foreground. refreshSession is the one Host scheduler shows: supabase.auth.session refreshes an expired token, the closure hands it to Set access token, and a refresh that fails skips the run. supabase comes from Supabase.swift in step 2 of Swift and Kotlin.

Parameters#

NameTypeRequiredDescription
(none)——This method takes no arguments. Everything a run needs comes from Initializing.

What a run does, in order#

NameTypeRequiredDescription
PushRepeated—Sends the head of the outbox, then repeats while the depth keeps dropping. A soft-blocked client sends nothing.
AttachmentsConditional—Runs only when the client was created with both attachmentRoot and a remote: it reclaims rows a dead process left claimed, then drives up to 32 queued uploads, so a ref reaches the server before its bytes are needed. Downloads are not driven here; they run through Resolve a download.
PullRepeated—Drains the keyset sequence page by page until the boundary closes, so a bootstrap larger than one page becomes visible in one run rather than one page per run.

Returns#

Void. The call is async throws. sync(), Pull once, and Push once serialize with each other and with the attachment transfer calls, so a second sync() waits for the first rather than running beside it. A local read or write answers at once, even while a sync() is in flight.

Errors#

CodeCondition
BUCKET_UNSETA table's bucket value is the empty string: a .byColumn bucket that Set bucket has not filled, or a .byOwner bucket before the first session token, at create or through Set access token, named an owner. See BUCKET_UNSET.
REMOTEA transport fault the client keeps retrying: network loss, a 5xx, an expired session (PGRST301), or 42501. The queued writes stay in the outbox.
PERMANENT_TRANSPORTThe server refused the request definitively: SQLSTATE class 22, 23, or 42 other than 42501, plus exact P0001 and 0A000. The fifth consecutive one against a single-entry push, or against an atomic batch, dead-letters it. A permanent failure on an unbatched multi-entry slice charges nothing directly: the slice is narrowed and retried at once until only the failing entry sends alone.
UNKNOWN_SIGNAL, MALFORMED_PUSH_RESPONSE, VERDICT_BIJECTION, UNKNOWN_BATCH_OFFENDER, UNKNOWN_VERDICT_REASON, UNKNOWN_OPThe response broke the wire contract. These fail loudly rather than counting against the dead-letter budget.
STORE, JSONThe local database or the response could not be read.
ENGINE_UNAVAILABLEThe client has no engine because Initializing has not run.

Notes#

A run raises LOCAL_CHANGED when the pull boundary commits, MUTATION_REJECTED once per refused write, BATCH_ABORTED for an atomic batch the server refused, DEAD_LETTER when the retry budget drops a write, COLUMN_OVERWRITTEN for each column-level conflict, and CHECKPOINT_EXPIRED or RESET_REQUIRED for a lifecycle signal. Subscribe with Subscribe to events to refresh a screen when one arrives.

The client has no timer of its own. On Swift, Host scheduler owns the poll interval, the network path, the foreground signal, and the wake after every local write, and it refreshes the Supabase session before each run so a run never rides a dead Bearer. An app that runs it calls sync() directly only where the gate and the refresh do not matter, such as a test; a Sync now button calls the scheduler's wake() instead. Protocol overview describes the two halves on the wire, and Consistency model explains what a closed checkpoint guarantees.

RESET_REQUIRED latches a soft block: the client stops making requests and applies nothing until Reset clears local state. A token of another user latches the same block with the reason identity_changed, as Set access token describes.

Found a problem? Edit this page on GitHub