Skip to content

Conversation

joshheald
Copy link
Contributor

Part of: WOOMOB-1088

Description

This PR adds a new datasource, analagous to PointOfSaleItemService, but with observation of the database so that any changes result in updates to the observable properties that GRDBObservableDataSource provides.

Steps to reproduce

This isn't used in the app yet. Unit tests are all you have! This PR is the base of a stack, and the top PR should allow you to use it in the item list, so functional testing can wait until then.


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

@joshheald joshheald added type: task An internally driven task. status: feature-flagged Behind a feature flag. Milestone is not strongly held. feature: POS labels Oct 9, 2025
@joshheald joshheald added this to the 23.5 milestone Oct 9, 2025
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Oct 9, 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 Numberpr16231-dce795b
Version23.4
Bundle IDcom.automattic.alpha.woocommerce
Commitdce795b
Installation URL096jrjv7lmvc0
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

joshheald and others added 3 commits October 9, 2025 17:19
Moves mock from YosemiteTests to PointOfSaleTests/Mocks where it can be used by PointOfSale module tests.

Changes:
- Moved file to Modules/Tests/PointOfSaleTests/Mocks/
- Made properties mutable for test control
- Removed public access (internal to test module)
- Simplified methods (tests set properties directly)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@joshheald joshheald force-pushed the issue/woomob-1088-use-GRDB-in-itemlist branch from 4e79a59 to a84aebc Compare October 9, 2025 16:23
@joshheald joshheald marked this pull request as ready for review October 9, 2025 16:52
Follow the pattern from PointOfSaleItemService by accepting CurrencySettings
and creating the PointOfSaleItemMapper internally. This avoids exposing
PointOfSaleItemMapper publicly from Yosemite.

Changes:
- GRDBObservableDataSource now takes currencySettings parameter
- Creates PointOfSaleItemMapper internally with optional override
- Updated tests to use new initializer
@joshheald joshheald requested a review from staskus October 10, 2025 08:11
@joshheald
Copy link
Contributor Author

joshheald commented Oct 10, 2025

@staskus I'll have the other PR in the stack up this morning, which will let you see it working in the views. You may want to delay testing until then.

I know you don't have all the context of this yet, so happy to have a call or async chat to go over anything

Replace sleep-based polling with proper Swift Observation tracking.
The waitForCondition helper now:
- Uses withObservationTracking to detect changes to observable properties
- Recursively re-observes until the condition is met
- Has a 2-second timeout as a backstop to prevent hanging
- Checks both loading completion AND expected item count
- No primary polling - purely event-driven with timeout safety

This fixes intermittent failures in:
- test_load_products_maps_to_pos_items_correctly
- test_load_products_sets_loading_state_and_fetches_items
Created reusable request methods on PersistedProduct and PersistedProductVariation:
- posProductsRequest: Filters to simple/variable, non-downloadable products, sorts by name
- posVariationsRequest: Filters to non-downloadable variations, sorts by ID

This fixes the pagination issue where unsupported/downloadable product types
caused fewer items than pageSize to be returned, stopping pagination early.

Before: 20 fetched → 18 mapped → pagination stopped (18 < 20)
After: 20 fetched → 20 mapped → pagination continues correctly

Changes:
- Add ProductType enum to PersistedProduct for type-safe filtering
- Filter out unsupported product types at DB level (not in mapper)
- Filter out downloadable products/variations
- Products sorted alphabetically by name (case-insensitive, localized)
- Variations sorted consistently by ID
- Use weak self capture in Task blocks to avoid retain cycles
- Centralized query logic for maintainability

Benefits:
- All supported products now load correctly with proper pagination
- Cleaner separation of concerns (filtering in DB, not mapper)
- More efficient (don't fetch items we'll filter out anyway)
Ensure the observe() function and condition check always run on MainActor
to prevent data races when accessing observable properties.

This fixes intermittent 'Index out of range' crashes on line 72 where
the continuation would resume before the productItems array was
actually populated on the main actor.
Use .receive(on: DispatchQueue.main) to ensure ValueObservation results
are delivered on the main queue, and update properties synchronously
rather than dispatching via Task { @mainactor }.

This simplifies the threading model and ensures property updates happen
synchronously when observations fire, which should improve reliability
and reduce potential race conditions.

Applied to all three observations: products, variations, and statistics.
@dangermattic
Copy link
Collaborator

1 Warning
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

.tracking { [weak self] database -> [POSProduct] in
guard let self else { return [] }

let persistedProducts = try PersistedProduct
Copy link
Contributor

Choose a reason for hiding this comment

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

It's a minor detail, but I wonder if this observation and the statistics observation should happen in the same observable to avoid any data inconsistencies. I imagine both updates should happen at the same time, but theoretically, there can be a gap between setting productItems and totalProductCount, making hasMoreProducts value wrong for a brief moment. 🤔

.fetchAll(database)

return try persistedProducts.map { persistedProduct in
let images = try persistedProduct.request(for: PersistedProduct.images).fetchAll(database)
Copy link
Contributor

Choose a reason for hiding this comment

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

For each product, we request images and attributes. Is this an effective way?

Could we flatten it all by:

  • Getting all products
  • Getting all attributes (for product ids) and make dictionary
  • Getting all images (for product ids) and make dictionary

Map into POSProduct with attributes and images.

Or I see there also .including method which maybe could be used.

Same for variations

.fetchCount(database)

let variationCount = try PersistedProductVariation
.filter(PersistedProductVariation.Columns.siteID == siteID)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should it be using posVariationsRequest or least .filter(Columns.downloadable == false) so we would get the same count in case there are downloadable variations?


I think it also affects hasMoreVariations since it checks totalVariationCount which is not filtered per-product and compares it to variationItems is is loaded per-product.

},
receiveValue: { [weak self] observedProducts in
guard let self else { return }
let posItems = itemMapper.mapProductsToPOSItems(products: observedProducts)
Copy link
Contributor

Choose a reason for hiding this comment

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

You mentioned the performance with a lot of items which may be connected to mapping, could we do the mapping outside the main thread and only receive on main thread?

    .publisher(in: grdbManager.databaseConnection)
    .map { [itemMapper] observedProducts in
        // Mapping happens on background Combine scheduler
        itemMapper.mapProductsToPOSItems(products: observedProducts)
    }
    .receive(on: DispatchQueue.main)

We could also leave such performance considerations for the future. I'm sure there are even more things that we could do if we reach use cases with extremely large catalogs.

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.

4 participants