Skip to content

Update ProductsRemote to load products with async/throws network methods to parse response in a background thread #15781

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 22 additions & 34 deletions Modules/Sources/Networking/Remote/ProductsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public protocol ProductsRemoteProtocol {
func addProduct(product: Product, completion: @escaping (Result<Product, Error>) -> Void)
func deleteProduct(for siteID: Int64, productID: Int64, completion: @escaping (Result<Product, Error>) -> Void)
func loadProduct(for siteID: Int64, productID: Int64, completion: @escaping (Result<Product, Error>) -> Void)
func loadProducts(for siteID: Int64, by productIDs: [Int64], pageNumber: Int, pageSize: Int, completion: @escaping (Result<[Product], Error>) -> Void)
func loadProducts(for siteID: Int64, by productIDs: [Int64], pageNumber: Int, pageSize: Int) async throws -> [Product]
func loadAllProducts(for siteID: Int64,
context: String?,
pageNumber: Int,
Expand All @@ -29,18 +29,15 @@ public protocol ProductsRemoteProtocol {
productStatus: ProductStatus?,
productType: ProductType?,
productCategory: ProductCategory?,
excludedProductIDs: [Int64],
completion: @escaping (Result<[Product], Error>) -> Void)
excludedProductIDs: [Int64]) async throws -> [Product]
func searchProductsBySKU(for siteID: Int64,
keyword: String,
pageNumber: Int,
pageSize: Int,
completion: @escaping (Result<[Product], Error>) -> Void)
pageSize: Int) async throws -> [Product]
func searchProductsByGlobalUniqueIdentifier(for siteID: Int64,
keyword: String,
pageNumber: Int,
pageSize: Int,
completion: @escaping (Result<[Product], Error>) -> Void)
pageSize: Int) async throws -> [Product]
func searchSku(for siteID: Int64,
sku: String,
completion: @escaping (Result<String, Error>) -> Void)
Expand Down Expand Up @@ -102,12 +99,11 @@ public protocol ProductsRemoteProtocol {
}

extension ProductsRemoteProtocol {
public func loadProducts(for siteID: Int64, by productIDs: [Int64], completion: @escaping (Result<[Product], Error>) -> Void) {
loadProducts(for: siteID,
by: productIDs,
pageNumber: ProductsRemote.Default.pageNumber,
pageSize: ProductsRemote.Default.pageSize,
completion: completion)
public func loadProducts(for siteID: Int64, by productIDs: [Int64]) async throws -> [Product] {
try await loadProducts(for: siteID,
by: productIDs,
pageNumber: ProductsRemote.Default.pageNumber,
pageSize: ProductsRemote.Default.pageSize)
}
}

Expand Down Expand Up @@ -387,16 +383,13 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
/// - productIDs: The array of product IDs that are requested.
/// - pageNumber: Number of page that should be retrieved.
/// - pageSize: Number of products to be retrieved per page.
/// - completion: Closure to be executed upon completion.
///
public func loadProducts(for siteID: Int64,
by productIDs: [Int64],
pageNumber: Int = Default.pageNumber,
pageSize: Int = Default.pageSize,
completion: @escaping (Result<[Product], Error>) -> Void) {
pageSize: Int = Default.pageSize) async throws -> [Product] {
guard productIDs.isEmpty == false else {
completion(.success([]))
return
return []
}

let stringOfProductIDs = productIDs.map { String($0) }
Expand All @@ -409,9 +402,9 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
]
let path = Path.products
let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true)
let mapper = ProductListMapper(siteID: siteID)
let mapper = ListMapper<Product>(siteID: siteID)

enqueue(request, mapper: mapper, completion: completion)
return try await enqueue(request, mapper: mapper)
}


Expand All @@ -438,7 +431,6 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
/// - pageNumber: Number of page that should be retrieved.
/// - pageSize: Number of products to be retrieved per page.
/// - excludedProductIDs: a list of product IDs to be excluded from the results.
/// - completion: Closure to be executed upon completion.
///
public func searchProducts(for siteID: Int64,
keyword: String,
Expand All @@ -448,8 +440,7 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
productStatus: ProductStatus? = nil,
productType: ProductType? = nil,
productCategory: ProductCategory? = nil,
excludedProductIDs: [Int64] = [],
completion: @escaping (Result<[Product], Error>) -> Void) {
excludedProductIDs: [Int64] = []) async throws -> [Product] {
let stringOfExcludedProductIDs = excludedProductIDs.map { String($0) }
.joined(separator: ",")

Expand All @@ -471,9 +462,9 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {

let path = Path.products
let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true)
let mapper = ProductListMapper(siteID: siteID)
let mapper = ListMapper<Product>(siteID: siteID)

enqueue(request, mapper: mapper, completion: completion)
return try await enqueue(request, mapper: mapper)
}

/// Retrieves all of the `Product`s that match the SKU. Partial SKU search is supported for WooCommerce version 6.6+, otherwise full SKU match is performed.
Expand All @@ -482,12 +473,10 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
/// - keyword: Search string that should be matched by the SKU (partial or full depending on the WC version).
/// - pageNumber: Number of page that should be retrieved.
/// - pageSize: Number of products to be retrieved per page.
/// - completion: Closure to be executed upon completion.
public func searchProductsBySKU(for siteID: Int64,
keyword: String,
pageNumber: Int,
pageSize: Int,
completion: @escaping (Result<[Product], Error>) -> Void) {
pageSize: Int) async throws -> [Product] {
let parameters = [
ParameterKey.sku: keyword,
ParameterKey.partialSKUSearch: keyword,
Expand All @@ -497,15 +486,14 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
]
let path = Path.products
let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true)
let mapper = ProductListMapper(siteID: siteID)
enqueue(request, mapper: mapper, completion: completion)
let mapper = ListMapper<Product>(siteID: siteID)
return try await enqueue(request, mapper: mapper)
}

public func searchProductsByGlobalUniqueIdentifier(for siteID: Int64,
keyword: String,
pageNumber: Int,
pageSize: Int,
completion: @escaping (Result<[Product], Error>) -> Void) {
pageSize: Int) async throws -> [Product] {
let parameters = [
ParameterKey.globalUniqueID: keyword,
ParameterKey.page: String(pageNumber),
Expand All @@ -514,8 +502,8 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
]
let path = Path.products
let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true)
let mapper = ProductListMapper(siteID: siteID)
enqueue(request, mapper: mapper, completion: completion)
let mapper = ListMapper<Product>(siteID: siteID)
return try await enqueue(request, mapper: mapper)
}

/// Retrieves a product SKU if available.
Expand Down
133 changes: 63 additions & 70 deletions Modules/Sources/Yosemite/Stores/ProductStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -231,35 +231,31 @@ private extension ProductStore {
productCategory: ProductCategory?,
excludedProductIDs: [Int64],
onCompletion: @escaping (Result<Bool, Error>) -> Void) {
switch filter {
case .all:
remote.searchProducts(for: siteID,
keyword: keyword,
pageNumber: pageNumber,
pageSize: pageSize,
stockStatus: stockStatus,
productStatus: productStatus,
productType: productType,
productCategory: productCategory,
excludedProductIDs: excludedProductIDs) { [weak self] result in
self?.handleSearchResults(siteID: siteID,
keyword: keyword,
filter: filter,
pageSize: pageSize,
result: result,
onCompletion: onCompletion)
}
case .sku:
remote.searchProductsBySKU(for: siteID,
keyword: keyword,
pageNumber: pageNumber,
pageSize: pageSize) { [weak self] result in
self?.handleSearchResults(siteID: siteID,
keyword: keyword,
filter: filter,
pageSize: pageSize,
result: result,
onCompletion: onCompletion)
Task { @MainActor in
do {
let products: [Product]
switch filter {
case .all:
products = try await remote.searchProducts(for: siteID,
keyword: keyword,
pageNumber: pageNumber,
pageSize: pageSize,
stockStatus: stockStatus,
productStatus: productStatus,
productType: productType,
productCategory: productCategory,
excludedProductIDs: excludedProductIDs)
case .sku:
products = try await remote.searchProductsBySKU(for: siteID,
keyword: keyword,
pageNumber: pageNumber,
pageSize: pageSize)
}
await upsertSearchResultsInBackground(siteID: siteID, keyword: keyword, filter: filter, readOnlyProducts: products)
let hasNextPage = products.count == pageSize
onCompletion(.success(hasNextPage))
} catch {
onCompletion(.failure(error))
}
}
}
Expand Down Expand Up @@ -340,13 +336,12 @@ private extension ProductStore {
return
}

remote.loadProducts(for: order.siteID, by: missingIDs) { [weak self] result in
switch result {
case .success(let products):
self?.upsertStoredProductsInBackground(readOnlyProducts: products, siteID: order.siteID, onCompletion: {
onCompletion(nil)
})
case .failure(let error):
Task { @MainActor in
do {
let products = try await remote.loadProducts(for: order.siteID, by: missingIDs)
await upsertStoredProductsInBackground(readOnlyProducts: products, siteID: order.siteID)
onCompletion(nil)
} catch {
onCompletion(error)
}
}
Expand All @@ -365,14 +360,13 @@ private extension ProductStore {
return
}

remote.loadProducts(for: siteID, by: productIDs, pageNumber: pageNumber, pageSize: pageSize) { [weak self] result in
switch result {
case .success(let products):
self?.upsertStoredProductsInBackground(readOnlyProducts: products, siteID: siteID, onCompletion: {
let hasNextPage = products.count == pageSize
onCompletion(.success((products: products, hasNextPage: hasNextPage)))
})
case .failure(let error):
Task { @MainActor in
do {
let products = try await remote.loadProducts(for: siteID, by: productIDs, pageNumber: pageNumber, pageSize: pageSize)
await upsertStoredProductsInBackground(readOnlyProducts: products, siteID: siteID)
let hasNextPage = products.count == pageSize
onCompletion(.success((products: products, hasNextPage: hasNextPage)))
} catch {
onCompletion(.failure(error))
}
}
Expand Down Expand Up @@ -1271,6 +1265,23 @@ private extension ProductStore {
}, completion: onCompletion, on: .main)
}

func upsertSearchResultsInBackground(siteID: Int64,
keyword: String,
filter: ProductSearchFilter,
readOnlyProducts: [Networking.Product]) async {
await withCheckedContinuation { [weak self] continuation in
guard let self else {
return continuation.resume()
}
upsertSearchResultsInBackground(siteID: siteID,
keyword: keyword,
filter: filter,
readOnlyProducts: readOnlyProducts) {
continuation.resume()
}
}
}

/// Upserts the Products, and associates them to the Search Results Entity (in the specified Storage)
///
private func upsertStoredResults(siteID: Int64,
Expand Down Expand Up @@ -1319,35 +1330,17 @@ private extension ProductStore {
}

func searchProductsBySKU(for siteID: Int64, keyword: String) async throws -> [Product] {
try await withCheckedThrowingContinuation { continuation in
remote.searchProductsBySKU(for: siteID,
keyword: keyword,
pageNumber: Remote.Default.firstPageNumber,
pageSize: ProductsRemote.Default.pageSize) { result in
switch result {
case let .success(products):
continuation.resume(returning: products)
case let .failure(error):
continuation.resume(throwing: error)
}
}
}
try await remote.searchProductsBySKU(for: siteID,
keyword: keyword,
pageNumber: Remote.Default.firstPageNumber,
pageSize: ProductsRemote.Default.pageSize)
}

func searchProductsByGlobalUniqueIdentifier(for siteID: Int64, keyword: String) async throws -> [Product] {
try await withCheckedThrowingContinuation { continuation in
remote.searchProductsByGlobalUniqueIdentifier(for: siteID,
keyword: keyword,
pageNumber: Remote.Default.firstPageNumber,
pageSize: ProductsRemote.Default.pageSize) { result in
switch result {
case let .success(products):
continuation.resume(returning: products)
case let .failure(error):
continuation.resume(throwing: error)
}
}
}
try await remote.searchProductsByGlobalUniqueIdentifier(for: siteID,
keyword: keyword,
pageNumber: Remote.Default.firstPageNumber,
pageSize: ProductsRemote.Default.pageSize)
}
}

Expand Down
Loading