Skip to content

Commit c69e785

Browse files
authored
[Local Catalog] Add incremental sync functionality to POSCatalogSyncCoordinator (#16117)
2 parents a9b92db + 54ca153 commit c69e785

File tree

4 files changed

+322
-20
lines changed

4 files changed

+322
-20
lines changed

Modules/Sources/Yosemite/Tools/POS/POSCatalogSyncCoordinator.swift

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ public protocol POSCatalogSyncCoordinatorProtocol {
1515
/// - maxAge: Maximum age before a sync is considered stale
1616
/// - Returns: True if a sync should be performed
1717
func shouldPerformFullSync(for siteID: Int64, maxAge: TimeInterval) async -> Bool
18+
19+
/// Performs an incremental sync if applicable based on sync conditions
20+
/// - Parameters:
21+
/// - siteID: The site ID to sync catalog for
22+
/// - forceSync: Whether to bypass age checks and always sync
23+
/// - Throws: POSCatalogSyncError.syncAlreadyInProgress if a sync is already running for this site
24+
//periphery:ignore - remove ignore comment when incremental sync is integrated with POS
25+
func performIncrementalSyncIfApplicable(for siteID: Int64, forceSync: Bool) async throws
1826
}
1927

2028
public enum POSCatalogSyncError: Error, Equatable {
@@ -23,26 +31,23 @@ public enum POSCatalogSyncError: Error, Equatable {
2331

2432
public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
2533
private let fullSyncService: POSCatalogFullSyncServiceProtocol
26-
private let persistenceService: POSCatalogPersistenceServiceProtocol
34+
private let incrementalSyncService: POSCatalogIncrementalSyncServiceProtocol
2735
private let grdbManager: GRDBManagerProtocol
36+
private let maxIncrementalSyncAge: TimeInterval
2837

29-
/// Tracks ongoing syncs by site ID to prevent duplicates
38+
/// Tracks ongoing full syncs by site ID to prevent duplicates
3039
private var ongoingSyncs: Set<Int64> = []
40+
/// Tracks ongoing incremental syncs by site ID to prevent duplicates
41+
private var ongoingIncrementalSyncs: Set<Int64> = []
3142

3243
public init(fullSyncService: POSCatalogFullSyncServiceProtocol,
33-
grdbManager: GRDBManagerProtocol) {
34-
self.fullSyncService = fullSyncService
35-
self.persistenceService = POSCatalogPersistenceService(grdbManager: grdbManager)
36-
self.grdbManager = grdbManager
37-
}
38-
39-
//periphery:ignore - used for tests to inject persistence service
40-
init(fullSyncService: POSCatalogFullSyncServiceProtocol,
41-
persistenceService: POSCatalogPersistenceServiceProtocol,
42-
grdbManager: GRDBManagerProtocol) {
44+
incrementalSyncService: POSCatalogIncrementalSyncServiceProtocol,
45+
grdbManager: GRDBManagerProtocol,
46+
maxIncrementalSyncAge: TimeInterval = 300) {
4347
self.fullSyncService = fullSyncService
44-
self.persistenceService = persistenceService
48+
self.incrementalSyncService = incrementalSyncService
4549
self.grdbManager = grdbManager
50+
self.maxIncrementalSyncAge = maxIncrementalSyncAge
4651
}
4752

4853
public func performFullSync(for siteID: Int64) async throws {
@@ -61,7 +66,7 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
6166

6267
DDLogInfo("🔄 POSCatalogSyncCoordinator starting full sync for site \(siteID)")
6368

64-
let catalog = try await fullSyncService.startFullSync(for: siteID)
69+
_ = try await fullSyncService.startFullSync(for: siteID)
6570

6671
DDLogInfo("✅ POSCatalogSyncCoordinator completed full sync for site \(siteID)")
6772
}
@@ -89,6 +94,40 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
8994
return shouldSync
9095
}
9196

97+
public func performIncrementalSyncIfApplicable(for siteID: Int64, forceSync: Bool) async throws {
98+
if ongoingIncrementalSyncs.contains(siteID) {
99+
DDLogInfo("⚠️ POSCatalogSyncCoordinator: Incremental sync already in progress for site \(siteID)")
100+
throw POSCatalogSyncError.syncAlreadyInProgress(siteID: siteID)
101+
}
102+
103+
guard let lastFullSyncDate = await lastFullSyncDate(for: siteID) else {
104+
DDLogInfo("📋 POSCatalogSyncCoordinator: No full sync performed yet for site \(siteID), skipping incremental sync")
105+
return
106+
}
107+
108+
if !forceSync, let lastIncrementalSyncDate = await lastIncrementalSyncDate(for: siteID) {
109+
let age = Date().timeIntervalSince(lastIncrementalSyncDate)
110+
111+
if age <= maxIncrementalSyncAge {
112+
return DDLogInfo("📋 POSCatalogSyncCoordinator: Last incremental sync for site \(siteID) was \(Int(age))s ago, sync not needed")
113+
}
114+
}
115+
116+
ongoingIncrementalSyncs.insert(siteID)
117+
118+
defer {
119+
ongoingIncrementalSyncs.remove(siteID)
120+
}
121+
122+
DDLogInfo("🔄 POSCatalogSyncCoordinator starting incremental sync for site \(siteID)")
123+
124+
try await incrementalSyncService.startIncrementalSync(for: siteID,
125+
lastFullSyncDate: lastFullSyncDate,
126+
lastIncrementalSyncDate: lastIncrementalSyncDate(for: siteID))
127+
128+
DDLogInfo("✅ POSCatalogSyncCoordinator completed incremental sync for site \(siteID)")
129+
}
130+
92131
// MARK: - Private
93132

94133
private func lastFullSyncDate(for siteID: Int64) async -> Date? {
@@ -102,6 +141,17 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
102141
}
103142
}
104143

144+
private func lastIncrementalSyncDate(for siteID: Int64) async -> Date? {
145+
do {
146+
return try await grdbManager.databaseConnection.read { db in
147+
return try PersistedSite.filter(key: siteID).fetchOne(db)?.lastCatalogIncrementalSyncDate
148+
}
149+
} catch {
150+
DDLogError("⛔️ POSCatalogSyncCoordinator: Error loading site \(siteID) for incremental sync date: \(error)")
151+
return nil
152+
}
153+
}
154+
105155
private func siteExistsInDatabase(siteID: Int64) -> Bool {
106156
do {
107157
return try grdbManager.databaseConnection.read { db in
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Foundation
2+
@testable import Yosemite
3+
4+
final class MockPOSCatalogIncrementalSyncService: POSCatalogIncrementalSyncServiceProtocol {
5+
var startIncrementalSyncResult: Result<Void, Error> = .success(())
6+
7+
private(set) var startIncrementalSyncCallCount = 0
8+
private(set) var lastSyncSiteID: Int64?
9+
private(set) var lastFullSyncDate: Date?
10+
private(set) var lastIncrementalSyncDate: Date?
11+
12+
private var syncContinuation: CheckedContinuation<Void, Never>?
13+
private var shouldBlockSync = false
14+
15+
func startIncrementalSync(for siteID: Int64, lastFullSyncDate: Date, lastIncrementalSyncDate: Date?) async throws {
16+
startIncrementalSyncCallCount += 1
17+
lastSyncSiteID = siteID
18+
self.lastFullSyncDate = lastFullSyncDate
19+
self.lastIncrementalSyncDate = lastIncrementalSyncDate
20+
21+
if shouldBlockSync {
22+
await withCheckedContinuation { continuation in
23+
syncContinuation = continuation
24+
}
25+
}
26+
27+
switch startIncrementalSyncResult {
28+
case .success:
29+
return
30+
case .failure(let error):
31+
throw error
32+
}
33+
}
34+
}
35+
36+
extension MockPOSCatalogIncrementalSyncService {
37+
func blockNextSync() {
38+
shouldBlockSync = true
39+
}
40+
41+
func resumeBlockedSync() {
42+
syncContinuation?.resume()
43+
syncContinuation = nil
44+
shouldBlockSync = false
45+
}
46+
}

0 commit comments

Comments
 (0)