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
- Use
syncOnReconnectfor reliability in unstable-network environments. - Add
syncOnFocusfor browser apps where users revisit tabs frequently. - Add
syncIntervalonly when freshness requirements justify battery/network cost. - 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.
Treat sync as eventually consistent. A successful local write does not guarantee remote completion until push and pull have finished.