Contents

philiprehberger/swift-sync-engine

Offline-first data sync engine with conflict resolution, retry queues, and local caching

Requirements

  • Swift >= 6.0
  • macOS 13+ / iOS 16+ / tvOS 16+ / watchOS 9+

Installation

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/philiprehberger/swift-sync-engine.git", from: "0.2.0")
]

Then add "SyncEngine" to your target dependencies:

.target(name: "YourTarget", dependencies: [
    .product(name: "SyncEngine", package: "swift-sync-engine")
])

Usage

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)")

Local Store

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()

Conflict Resolution

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
}))

Retry Queue

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)")

Progress Reporting

let result = try engine.sync(
    push: { records in api.upload(records) },
    pull: { api.fetchChanges() },
    onProgress: { current, total in
        print("Progress: \(current)/\(total)")
    }
)

Query and Bulk Operations

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)

Sync Records

var record = SyncRecord(id: "doc-1", data: ["content": "Hello"], version: 1)
record = record.incrementVersion()  // version 2, updated timestamp
record = record.withStatus(.synced)

API

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 |

Development

swift build
swift test

Support

If you find this project useful:

⭐ Star the repo

πŸ› Report issues

πŸ’‘ Suggest features

❀️ Sponsor development

🌐 All Open Source Projects

πŸ’» GitHub Profile

πŸ”— LinkedIn Profile

License

MIT

Package Metadata

Repository: philiprehberger/swift-sync-engine

Default branch: main

README: README.md