This document explains how FlutterSync's layers fit together. For the high-level diagram, see the README.
- Caller invokes
SyncRepository.save(model). - The repository serializes the model with the
SyncModelSerializer. OptimisticUpdateManager.applyOptimisticreads the current local state (for rollback), stamps the write with a freshHLCTimestampfrom the clock, and persists the new record throughSyncStore.upsert.- The encryptor (if configured) wraps protected fields.
- The record is enqueued in the outbox (
OutboxQueue.enqueue) with operationupsertand an idempotency key derived from(collection, id, hlc). SyncRepository.savereturns success — without waiting for the network.- In the background, the
SyncSchedulerticks,OutboxProcessordrains the queue, and the adapter sends batches to the server.
- Caller invokes
SyncRepository.findAll(query). - The repository forwards the query to
SyncStore.findAll. - Records flow through the encryptor's
decryptif needed. - The repository deserializes each record back into the typed
Tmodel.
Reads never block on the network — they always come from the local store.
SyncScheduler.syncNow(or a periodic tick) callsSyncEngine.syncNow.- For each collection with metadata, the engine reads
SyncMetadata.lastSyncedAtand builds aSyncPullRequestwithsince: lastSyncedAt. - The adapter returns a
SyncPullResult. DeltaMergerintegrates each remote record:clock.receive(remote.hlc)to advance the local HLC.- If no local record exists → insert.
- If the remote dominates by HLC → apply.
- If the local dominates → ignore.
- Otherwise → real conflict → call
ConflictResolver.resolve→ apply the winner.
SyncMetadata.lastSyncedAtis advanced to the highest HLC observed in the batch.
- 64-bit physical time (milliseconds since the Unix epoch) + 32-bit logical counter + UUID v4
nodeId. - Wire format zero-pads physical and counter to 20 and 10 digits respectively so lexicographic string comparison matches numerical comparison.
- Drift detection: a remote whose
physicalTimeexceeds the local wall clock by more than the configured tolerance throwsHLCDriftExceptionso the system can surface the misconfiguration rather than silently jumping forward.
- Backed by
OutboxQueue(in-memory by default; the Drift-backed variant ships inDriftSyncStore). - Each
OutboxEntrycarries the full record, an operation tag (upsert/delete), a status (pending,inflight,synced,failed), retry bookkeeping, and an idempotency key. ExponentialBackoffRetryStrategycomputesmin(baseDelay * 2^attempts + jitter, maxDelay)between attempts.- After
maxAttempts(default 20), the entry is dead-lettered and surfaced via theonFailurecallback so the application can decide what to do.
ForegroundSyncis a simpleTimer.periodicdriver that triggers a single processor pass each tick, with reentrancy guards so two ticks never overlap.BackgroundSyncis platform-specific (Androidworkmanager, iOSbackground_fetch, etc.) and runs even when the app is in the background.ConnectivityObserverdebouncesconnectivity_plusevents so flaky transitions do not trigger storms of sync attempts.BandwidthMonitorkeeps a rolling window of measured throughput per network state and recommends a batch size that fits the target push duration.
- Argon2id key derivation runs in
Isolate.runso the UI thread stays responsive while the CPU-intensive derivation runs. - Future versions will move large delta computations (>1000 records) into an isolate; today the computation runs on the calling isolate because it is dominated by I/O.
lib/flutter_sync.dart is the single source of truth for what is public. Anything under lib/src/ that the barrel does not re-export may change between minor releases without notice. Adapters that ship as separate packages would import the barrel; in-tree adapters import their nearest public symbols (SyncRecord, SyncBatch, etc.) directly.