Skip to content

Commit 0232f01

Browse files
committed
Observe catalog sync state using Observation model
1 parent 90f2949 commit 0232f01

File tree

6 files changed

+44
-58
lines changed

6 files changed

+44
-58
lines changed

Modules/Sources/PointOfSale/Models/PointOfSaleAggregateModel.swift

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import protocol Yosemite.PointOfSaleBarcodeScanServiceProtocol
1616
import enum Yosemite.PointOfSaleBarcodeScanError
1717
import protocol Yosemite.POSCatalogSyncCoordinatorProtocol
1818
import enum Yosemite.POSCatalogSyncState
19+
import class Yosemite.POSCatalogSyncCoordinator
1920

2021
protocol PointOfSaleAggregateModelProtocol {
2122
var cart: Cart { get }
@@ -63,6 +64,7 @@ protocol PointOfSaleAggregateModelProtocol {
6364
private let soundPlayer: PointOfSaleSoundPlayerProtocol
6465

6566
private var cancellables: Set<AnyCancellable> = []
67+
private var catalogSyncStateTask: Task<Void, Never>?
6668

6769
// Private storage of the concrete coordinator
6870
private let _viewStateCoordinator = PointOfSaleViewStateCoordinator()
@@ -117,7 +119,7 @@ protocol PointOfSaleAggregateModelProtocol {
117119
setupReaderReconnectionObservation()
118120
setupPaymentSuccessObservation()
119121
performIncrementalSync()
120-
publishCatalogSyncState()
122+
setupCatalogSyncStateObservation()
121123
}
122124
}
123125

@@ -601,6 +603,7 @@ extension PointOfSaleAggregateModel {
601603
// cancelling them explicitly helps reduce the risk of user-visible bugs while we work on the memory leaks.
602604
resetCardReaderObservation()
603605
cancellables.forEach { $0.cancel() }
606+
catalogSyncStateTask?.cancel()
604607
}
605608
}
606609

@@ -630,17 +633,26 @@ private extension PointOfSaleAggregateModel {
630633
// MARK: - Catalog Sync State Observation
631634

632635
private extension PointOfSaleAggregateModel {
633-
private func publishCatalogSyncState() {
636+
private func setupCatalogSyncStateObservation() {
634637
guard let coordinator = catalogSyncCoordinator else { return }
635638

636-
Task { @MainActor [weak self] in
637-
guard let self else { return }
638-
639+
Task { @MainActor in
639640
catalogSyncState = await coordinator.lastFullSyncState(for: siteID)
641+
}
642+
643+
observeCatalogSyncState()
644+
}
640645

641-
for await state in coordinator.fullSyncStateStream {
646+
@Sendable private func observeCatalogSyncState() {
647+
withObservationTracking { [weak self] in
648+
guard let self else { return }
649+
650+
if let state = catalogSyncCoordinator?.fullSyncStateModel.state[siteID] {
642651
catalogSyncState = state
643652
}
653+
} onChange: { [weak self] in
654+
guard let self else { return }
655+
DispatchQueue.main.async(execute: observeCatalogSyncState)
644656
}
645657
}
646658
}

Modules/Sources/PointOfSale/Utils/PreviewHelpers.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import class Yosemite.POSOrderListService
3030
import class Yosemite.POSOrderListFetchStrategyFactory
3131
import protocol Yosemite.POSCatalogSyncCoordinatorProtocol
3232
import enum Yosemite.POSCatalogSyncState
33+
import class Yosemite.POSCatalogSyncStateModel
3334
import protocol Yosemite.POSCatalogSettingsServiceProtocol
3435
import struct Yosemite.POSCatalogInfo
3536
import struct Yosemite.Site
@@ -629,13 +630,10 @@ final class POSPreviewCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol
629630
try await Task.sleep(nanoseconds: 1_000_000_000)
630631
}
631632

632-
let fullSyncStateStream: AsyncStream<POSCatalogSyncState> = {
633-
let (stream, _) = AsyncStream<POSCatalogSyncState>.makeStream()
634-
return stream
635-
}()
633+
let fullSyncStateModel = POSCatalogSyncStateModel()
636634

637635
func lastFullSyncState(for siteID: Int64) async -> POSCatalogSyncState {
638-
return .syncCompleted(siteID: siteID)
636+
return fullSyncStateModel.state[siteID] ?? .syncCompleted(siteID: siteID)
639637
}
640638
}
641639

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

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public protocol POSCatalogSyncCoordinatorProtocol {
2929
func performSmartSync(for siteID: Int64, fullSyncMaxAge: TimeInterval, incrementalSyncMaxAge: TimeInterval) async throws
3030

3131
/// Stream that emits full sync state updates
32-
var fullSyncStateStream: AsyncStream<POSCatalogSyncState> { get }
32+
var fullSyncStateModel: POSCatalogSyncStateModel { get }
3333

3434
/// Returns the last known full sync state for a site
3535
/// If no state is cached, determines state from lastSyncDate
@@ -70,12 +70,8 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
7070
/// Tracks ongoing incremental syncs by site ID to prevent duplicates
7171
private var ongoingIncrementalSyncs: Set<Int64> = []
7272

73-
/// Stream for full sync state updates
74-
public nonisolated let fullSyncStateStream: AsyncStream<POSCatalogSyncState>
75-
/// Continuation for emitting state updates
76-
private let fullSyncStateStreamContinuation: AsyncStream<POSCatalogSyncState>.Continuation
77-
/// Cache of last known full sync state for each site
78-
private var fullSyncStateCache: [Int64: POSCatalogSyncState] = [:]
73+
/// Observable model for full sync state updates
74+
public nonisolated let fullSyncStateModel: POSCatalogSyncStateModel = .init()
7975

8076
public init(fullSyncService: POSCatalogFullSyncServiceProtocol,
8177
incrementalSyncService: POSCatalogIncrementalSyncServiceProtocol,
@@ -87,10 +83,6 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
8783
self.grdbManager = grdbManager
8884
self.catalogSizeLimit = catalogSizeLimit ?? Constants.defaultSizeLimitForPOSCatalog
8985
self.catalogSizeChecker = catalogSizeChecker
90-
91-
let (stream, continuation) = AsyncStream<POSCatalogSyncState>.makeStream()
92-
self.fullSyncStateStream = stream
93-
self.fullSyncStateStreamContinuation = continuation
9486
}
9587

9688
public func performFullSyncIfApplicable(for siteID: Int64, maxAge: TimeInterval) async throws {
@@ -102,7 +94,7 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
10294
return
10395
}
10496

105-
if case .syncStarted = fullSyncStateCache[siteID] {
97+
if case .syncStarted = fullSyncStateModel.state[siteID] {
10698
DDLogInfo("⚠️ POSCatalogSyncCoordinator: Sync already in progress for site \(siteID)")
10799
throw POSCatalogSyncError.syncAlreadyInProgress(siteID: siteID)
108100
}
@@ -309,7 +301,7 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
309301
}
310302

311303
public func lastFullSyncState(for siteID: Int64) async -> POSCatalogSyncState {
312-
if let cached = fullSyncStateCache[siteID] {
304+
if let cached = fullSyncStateModel.state[siteID] {
313305
return cached
314306
}
315307

@@ -330,11 +322,17 @@ private extension POSCatalogSyncCoordinator {
330322
id
331323
}
332324

333-
fullSyncStateCache[siteID] = state
334-
fullSyncStateStreamContinuation.yield(state)
325+
fullSyncStateModel.state[siteID] = state
335326
}
336327
}
337328

329+
@Observable
330+
public class POSCatalogSyncStateModel {
331+
public var state: [Int64: POSCatalogSyncState] = [:]
332+
333+
public init() {}
334+
}
335+
338336
public enum POSCatalogSyncState: Equatable {
339337
case syncStarted(siteID: Int64, isInitialSync: Bool)
340338
case syncCompleted(siteID: Int64)

Modules/Tests/PointOfSaleTests/Mocks/MockPOSCatalogSyncCoordinator.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,9 @@ final class MockPOSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
6363
}
6464
}
6565

66-
let fullSyncStateStream: AsyncStream<POSCatalogSyncState> = {
67-
let (stream, _) = AsyncStream<POSCatalogSyncState>.makeStream()
68-
return stream
69-
}()
66+
let fullSyncStateModel = POSCatalogSyncStateModel()
7067

7168
func lastFullSyncState(for siteID: Int64) async -> POSCatalogSyncState {
72-
return .syncNeverDone(siteID: siteID)
69+
return fullSyncStateModel.state[siteID] ?? .syncNeverDone(siteID: siteID)
7370
}
7471
}

Modules/Tests/YosemiteTests/Tools/POS/POSCatalogSyncCoordinatorTests.swift

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,8 @@ struct POSCatalogSyncCoordinatorTests {
343343

344344
@Test func performIncrementalSyncIfApplicable_skips_sync_when_incremental_sync_is_within_max_age() async throws {
345345
// Given
346-
let maxAge: TimeInterval = 2
347-
let incrementalSyncDate = Date().addingTimeInterval(-(maxAge - 0.2)) // Just within max age
346+
let maxAge: TimeInterval = 60 // 60 seconds
347+
let incrementalSyncDate = Date().addingTimeInterval(-30) // 30 seconds ago (within maxAge)
348348
let fullSyncDate = Date().addingTimeInterval(-7200)
349349
try createSiteInDatabase(siteID: sampleSiteID, lastFullSyncDate: fullSyncDate, lastIncrementalSyncDate: incrementalSyncDate)
350350

@@ -358,7 +358,7 @@ struct POSCatalogSyncCoordinatorTests {
358358
// When
359359
try await sut.performIncrementalSyncIfApplicable(for: sampleSiteID, maxAge: maxAge)
360360

361-
// Then
361+
// Then - should not sync because 30 seconds < 60 second maxAge
362362
#expect(mockIncrementalSyncService.startIncrementalSyncCallCount == 0)
363363
}
364364

@@ -716,32 +716,16 @@ struct POSCatalogSyncCoordinatorTests {
716716
#expect(state == .syncCompleted(siteID: sampleSiteID))
717717
}
718718

719-
@Test func fullSyncStateStream_emits_events_during_sync() async throws {
719+
@Test func fullSyncStateModel_emits_events_during_sync() async throws {
720720
// Given
721721
let expectedCatalog = POSCatalog(products: [], variations: [], syncDate: .now)
722722
mockSyncService.startFullSyncResult = .success(expectedCatalog)
723723

724724
// When - start sync and stream collection concurrently
725-
async let syncResult: Void = sut.performFullSync(for: sampleSiteID)
726-
727-
var receivedEvents: [POSCatalogSyncState] = []
728-
let collectTask = Task {
729-
for await state in sut.fullSyncStateStream {
730-
receivedEvents.append(state)
731-
// Stop after getting completion
732-
if case .syncCompleted(let id) = state, id == self.sampleSiteID {
733-
break
734-
}
735-
}
736-
}
737-
738-
// Wait for sync and stream collection
739-
try await syncResult
740-
_ = await collectTask.value
725+
try await sut.performFullSync(for: sampleSiteID)
741726

742727
// Then - should emit syncStarted and syncCompleted with correct siteID
743-
#expect(receivedEvents.contains(where: { if case .syncStarted(let id, _) = $0 { return id == self.sampleSiteID } else { return false } }))
744-
#expect(receivedEvents.contains { $0 == .syncCompleted(siteID: self.sampleSiteID) })
728+
#expect(sut.fullSyncStateModel.state[sampleSiteID] == .syncCompleted(siteID: sampleSiteID))
745729
}
746730

747731
// MARK: - Helper Methods

WooCommerce/WooCommerceTests/Tools/ForegroundPOSCatalogSyncDispatcherTests.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,12 +292,9 @@ private final class MockPOSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProt
292292
// Not used
293293
}
294294

295-
let fullSyncStateStream: AsyncStream<POSCatalogSyncState> = {
296-
let (stream, _) = AsyncStream<POSCatalogSyncState>.makeStream()
297-
return stream
298-
}()
295+
let fullSyncStateModel = POSCatalogSyncStateModel()
299296

300297
func lastFullSyncState(for siteID: Int64) async -> POSCatalogSyncState {
301-
return .syncNeverDone(siteID: siteID)
298+
return fullSyncStateModel.state[siteID] ?? .syncNeverDone(siteID: siteID)
302299
}
303300
}

0 commit comments

Comments
 (0)