Skip to content

Conversation

@jaclync
Copy link
Contributor

@jaclync jaclync commented Sep 11, 2025

For WOOMOB-1252

⚠️ Based on #16115

Description

This PR adds incremental sync functionality to the POSCatalogSyncCoordinator by integrating with POSCatalogIncrementalSyncService which only syncs changes since the last sync.

The implementation adds a new performIncrementalSyncIfApplicable method that intelligently determines when incremental sync is appropriate based on:

  • Whether a full sync has been previously performed
  • The age of the last incremental sync (configurable via dependency injection)
  • A force sync option to bypass age checks when needed

Key Changes

  • New functionality: Added performIncrementalSyncIfApplicable(for:forceSync:) method to sync coordinator
    • I combined the "should perform sync" logic inside performIncrementalSyncIfApplicable so that the call site just makes one call when it's appropriate to trigger an incremental sync. Feel free to share any thoughts on this!
  • Sync logic: Implements conditions to determine when incremental sync should run:
    1. No ongoing incremental sync for the site
    2. A full sync has been performed previously (last full sync date exists)
    3. Sufficient time has passed since last incremental sync (respects maxIncrementalSyncAge) (I set it to 5 minutes tentatively)
  • Force sync support: forceSync parameter bypasses age checks for immediate sync when needed
  • Testing: Added test cases covering sync conditions and edge cases

Steps to reproduce

As incremental sync hasn't been integrated with POS yet, it can be tested by adding debug code below MainTabBarController.swift#L315 to trigger an incremental sync when tapping on the POS tab:

Task { @MainActor in
    do {
        try await posCatalogSyncCoordinator?.performIncrementalSyncIfApplicable(for: ServiceLocator.stores.sessionManager.defaultStoreID!, forceSync: false)
        print("Incremental sync succeeded")
    } catch {
        print("Incremental sync failed: \(error)")
    }
}

And then:

  • Launch the app, log in to a store eligible for POS if needed, and ensure a full sync has been performed successfully by checking the console
  • In the Products tab or wp-admin, update at least one variation of a variable product (e.g. price, inventory) and save
  • Tap on the POS tab --> it should trigger an incremental sync, and the console logs should indicate that 1 product at least 1 variation have been synced
  • Exit POS
  • Tap on the POS tab again --> the console logs should indicate that the sync is not needed with the last incremental sync date

Testing information

I tested the above steps in iPad A16 iOS 18.4 simulator.


  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

@jaclync jaclync changed the base branch from trunk to woomob-1252-sync-coordinator-store-sync-date-on-site September 11, 2025 05:28
@jaclync jaclync changed the title POSCatalogSyncCoordinator: Add incremental sync functionality [Local Catalog] Add incremental sync functionality to POSCatalogSyncCoordinator Sep 11, 2025
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Sep 11, 2025

App Icon📲 You can test the changes from this Pull Request in WooCommerce iOS Prototype by scanning the QR code below to install the corresponding build.

App NameWooCommerce iOS Prototype
Build Numberpr16117-54ca153
Version23.2
Bundle IDcom.automattic.alpha.woocommerce
Commit54ca153
Installation URL65vngotrej7t0
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@jaclync jaclync added type: task An internally driven task. status: feature-flagged Behind a feature flag. Milestone is not strongly held. feature: POS labels Sep 11, 2025
@jaclync jaclync added this to the 23.3 milestone Sep 11, 2025
@jaclync jaclync requested a review from joshheald September 11, 2025 06:37
@joshheald joshheald force-pushed the woomob-1252-sync-coordinator-store-sync-date-on-site branch from a467350 to 0ffe5d7 Compare September 11, 2025 09:20
@joshheald joshheald self-assigned this Sep 11, 2025
Base automatically changed from woomob-1252-sync-coordinator-store-sync-date-on-site to trunk September 11, 2025 11:09
Comment on lines +38 to +41
/// Tracks ongoing full syncs by site ID to prevent duplicates
private var ongoingSyncs: Set<Int64> = []
/// Tracks ongoing incremental syncs by site ID to prevent duplicates
private var ongoingIncrementalSyncs: Set<Int64> = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know whether we really want to be able to do a full sync and an incremental sync for the same site simultaneously.

  1. Start a full sync that takes 1 minute to download
  2. 10s later, start an incremental sync which takes 30s to download
  3. 40s after starting, the incremental sync changes are written to the db
  4. 1m after starting, the full sync write starts – it deletes everything in the db for that site, and puts new data in.

Let's tackle in a separate ticket, as it's an edge case, but WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know whether we really want to be able to do a full sync and an incremental sync for the same site simultaneously.

Yea good point, incremental sync should be skipped when a full sync is ongoing. Created an issue WOOMOB-1331.

Comment on lines +98 to +114
if ongoingIncrementalSyncs.contains(siteID) {
DDLogInfo("⚠️ POSCatalogSyncCoordinator: Incremental sync already in progress for site \(siteID)")
throw POSCatalogSyncError.syncAlreadyInProgress(siteID: siteID)
}

guard let lastFullSyncDate = await lastFullSyncDate(for: siteID) else {
DDLogInfo("📋 POSCatalogSyncCoordinator: No full sync performed yet for site \(siteID), skipping incremental sync")
return
}

if !forceSync, let lastIncrementalSyncDate = await lastIncrementalSyncDate(for: siteID) {
let age = Date().timeIntervalSince(lastIncrementalSyncDate)

if age <= maxIncrementalSyncAge {
return DDLogInfo("📋 POSCatalogSyncCoordinator: Last incremental sync for site \(siteID) was \(Int(age))s ago, sync not needed")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth moving this to a shouldPerform function, even if it's only used privately in this class and not exposed.

I think we should probably use the same approach for both – I don't always enjoy the "if applicable/if needed" uncertainty, but then it is the job of this coordinator to manage when syncs happen, so it's not unreasonable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think are the use cases / benefits of a separate shouldPerform that is public? It'd be useful for the call site to handle the result, but I don't think we plan to show any UI for it. A private shouldPerform function could make the code more readable though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think giving both a private shouldPerform is the best approach for now.

We probably don't ever need to make them public, but some possible future reasons:

  • To log analytics when a background task runs about whether we synced, or why we didn't.
  • To show the status in settings "will sync" or make different buttons available.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think giving both a private shouldPerform is the best approach for now.

Sounds good, we can make it public when the use case calls for it. I created an issue WOOMOB-1333 for making this change.

Comment on lines +144 to +153
private func lastIncrementalSyncDate(for siteID: Int64) async -> Date? {
do {
return try await grdbManager.databaseConnection.read { db in
return try PersistedSite.filter(key: siteID).fetchOne(db)?.lastCatalogIncrementalSyncDate
}
} catch {
DDLogError("⛔️ POSCatalogSyncCoordinator: Error loading site \(siteID) for incremental sync date: \(error)")
return nil
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this, removing the loadSite/updateSite functions from the persistence service, and saving the dates as part of the catalog, not afterwards.

It's worked out to be much simpler IMO, but I wanted to bring it to your attention, so let me know if you think we should go back

@joshheald joshheald merged commit c69e785 into trunk Sep 11, 2025
14 checks passed
@joshheald joshheald deleted the feat/WOOMOB-1252-sync-coordinator-incremental-sync branch September 11, 2025 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature: POS status: feature-flagged Behind a feature flag. Milestone is not strongly held. type: task An internally driven task.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants