Sync lifecycle

Sync keeps your local data and remote data aligned while preserving local-first behavior. In FFDB, sync is explicit and observable: you can trigger it directly, subscribe to status, and react in UI or service logic.

Why sync control matters

Apps need different sync timing guarantees:

  • Name
    User-driven sync
    Description

    Useful when users manually refresh data or complete an important workflow.

  • Name
    Automatic background sync
    Description

    Keeps data fresh on reconnect, focus, or interval without extra user steps.

  • Name
    Deterministic completion points
    Description

    Lets you wait for sync completion before navigating or confirming state.

Sync handle interface

type SyncHandle = {
  run: () => Promise<{ pushed: number; failed: number }>
  sync: () => Promise<{ pushed: number; failed: number }>
  pull: () => Promise<void>
  push: () => Promise<{ pushed: number; failed: number }>
  waitForIdle: (timeoutMs?: number) => Promise<void>
  readonly status: {
    isSyncing: boolean
    isOnline: boolean
    lastSyncedAt: number | null
    pendingMutations: number
    error: Error | null
  }
  subscribe: (listener: () => void) => () => void
}

run() and sync() are equivalent aliases for full sync behavior.

Method semantics

  • Name
    pull()
    Description

    Fetches fresh remote data and reconciles local cache.

  • Name
    push()
    Description

    Sends queued local mutations and then reconciles via pull when needed.

  • Name
    sync() / run()
    Description

    Full cycle: push pending mutations first, then pull fresh data.

  • Name
    waitForIdle(timeout)
    Description

    Blocks until sync finishes or timeout is reached.

  • Name
    subscribe(listener)
    Description

    Emits on status changes so UI can show progress and errors.

End-to-end orchestration example

import { createClient } from 'ffdb-client'
import type { Database } from './ffdb.types'

const { db, sync } = await createClient<Database>({
  config: { apiUrl: import.meta.env.VITE_FFDB_API_URL },
  offline: {
    adapter: offlineAdapter,
    syncOnReconnect: true,
    syncOnFocus: true,
    syncInterval: 30_000,
  },
})

await db
  .insertInto('task')
  .values({ id: crypto.randomUUID(), title: 'Draft report', completed: 0 })
  .execute()

if (sync) {
  const unsubscribe = sync.subscribe(() => {
    const s = sync.status
    console.log('sync-status', {
      isSyncing: s.isSyncing,
      pendingMutations: s.pendingMutations,
      lastSyncedAt: s.lastSyncedAt,
      error: s.error?.message,
    })
  })

  // Force a full cycle after a critical write.
  const result = await sync.run()
  console.log(result) // { pushed, failed }

  await sync.waitForIdle(10_000)
  unsubscribe()
}

How to choose a trigger strategy

  1. Use syncOnReconnect for reliability in unstable-network environments.
  2. Add syncOnFocus for browser apps where users revisit tabs frequently.
  3. Add syncInterval only when freshness requirements justify battery/network cost.
  4. Use manual sync.run() after user actions that must appear remotely quickly.

Error and recovery guidance

  • Name
    Check status.error
    Description

    Surface meaningful error states in the UI instead of silent retries.

  • Name
    Watch pendingMutations
    Description

    Rising pending counts usually indicate connectivity or auth readiness issues.

  • Name
    Use waitForIdle for deterministic steps
    Description

    Helpful before route transitions, report generation, or post-submit confirmations.

Next pages

  1. Mutation queue
  2. Conflict behavior
  3. React

Was this page helpful?