Offline-first data sync engine with conflict resolution, retry queues, and local caching
.package(url: "https://github.com/philiprehberger/swift-sync-engine.git", from: "0.2.0")Offline-first data sync engine with conflict resolution, retry queues, and local caching
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/philiprehberger/swift-sync-engine.git", from: "0.1.0")
]
Then add "SyncEngine" to your target dependencies:
.target(name: "YourTarget", dependencies: [
.product(name: "SyncEngine", package: "swift-sync-engine")
])
import SyncEngine
let engine = SyncEngine()
// Store data locally
engine.localStore.put(SyncRecord(id: "user-1", data: ["name": "Alice"]))
// Sync with your backend
let result = try engine.sync(
push: { records in myAPI.upload(records) },
pull: { myAPI.fetchChanges() }
)
print("Pushed: \(result.pushed), Pulled: \(result.pulled), Conflicts: \(result.conflicts)")
Store and query records offline:
let store = engine.localStore
store.put(SyncRecord(id: "1", data: ["title": "Draft"]))
store.markModified("1") // flag as changed locally
let pending = store.pending() // records needing sync
let all = store.all()
Choose how to handle conflicts when local and remote diverge:
// Remote always wins
let engine = SyncEngine(resolver: ConflictResolver(strategy: .remoteWins))
// Local always wins
let engine = SyncEngine(resolver: ConflictResolver(strategy: .localWins))
// Most recent wins (default)
let engine = SyncEngine(resolver: ConflictResolver(strategy: .latestWins))
// Custom merge
let engine = SyncEngine(resolver: ConflictResolver(strategy: .custom { local, remote in
var merged = local
merged.data.merge(remote.data) { _, new in new }
return merged
}))
Failed push operations are automatically queued for retry:
let engine = SyncEngine(queue: RetryQueue(maxAttempts: 5))
// After a failed sync, items are in the retry queue
print(engine.retryQueue.count)
// They'll be retried on the next sync cycle
let result = try engine.sync(push: myAPI.upload, pull: myAPI.fetch)
print("Retried: \(result.retried)")
let result = try engine.sync(
push: { records in api.upload(records) },
pull: { api.fetchChanges() },
onProgress: { current, total in
print("Progress: \(current)/\(total)")
}
)
let users = engine.localStore.query { $0.data["type"] == "user" }
engine.localStore.putAll(records)
let stats = engine.localStore.statistics // (total: 10, pending: 2, synced: 7, modified: 1)
var record = SyncRecord(id: "doc-1", data: ["content": "Hello"], version: 1)
record = record.incrementVersion() // version 2, updated timestamp
record = record.withStatus(.synced)
SyncEngine| Method | Description |
|---|---|
SyncEngine(store:queue:resolver:) | Create with optional custom components |
.sync(push:pull:) | Perform a full sync cycle |
.localStore | Access the local store |
.retryQueue | Access the retry queue |
.conflictResolver | Access the conflict resolver |
.isSyncing | Whether a sync is in progress |
.sync(push:pull:onProgress:) | Sync with progress callback |
.lastSyncResult | Most recent sync result |
LocalStore| Method | Description |
|---|---|
.put(_:) | Store or update a record |
.get(_:) | Retrieve by ID |
.remove(_:) | Remove by ID |
.all() | Get all records |
.pending() | Get pending/modified records |
.markSynced(_:) | Mark as synced |
.markModified(_:) | Mark as locally modified |
.clear() | Remove all records |
.query(where:) | Filter records by predicate |
.putAll(_:) | Bulk insert records |
.statistics | Count by status (total, pending, synced, modified) |
ConflictResolver| Method | Description |
|---|---|
.resolve(local:remote:) | Resolve a conflict between two records |
.strategy | Get/set the resolution strategy |
.resolvedCount | Number of conflicts resolved |
RetryQueue| Method | Description |
|---|---|
.enqueue(_:) | Add a failed record to retry |
.dequeueAll() | Remove and return all queued records |
.pending() | Peek at queued items |
.count | Number of queued items |
swift build
swift test
If you find this project useful: