Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import Foundation

/// Protocol for abstracting time-based operations to enable testing
protocol Clock {
protocol Clock: Sendable {
/// Sleep for the specified number of nanoseconds
func sleep(nanoseconds: UInt64) async throws
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func exponentialDelay(for attempt: Int = 1, with retryDelay: TimeInterval) -> UI
return UInt64(delayInNanoseconds)
}

extension Task where Failure == Error {
extension Task where Failure == Error, Success: Sendable {
@discardableResult static func retrying(
priority: TaskPriority? = nil,
maxRetryCount: Int = 3,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import ShopifyCheckoutKit

/// A lightweight GraphQL client for the Storefront API without external dependencies
@available(iOS 16.0, *)
class GraphQLClient {
final class GraphQLClient: Sendable {
let url: URL
private let headers: [String: String]
private let session: URLSession
Expand All @@ -53,19 +53,19 @@ class GraphQLClient {
/// Execute a GraphQL query
/// - Parameter operation: The GraphQL query operation
/// - Returns: The decoded response
func query<T: Decodable>(_ operation: GraphQLRequest<T>) async throws -> GraphQLResponse<T> {
func query<T: Decodable & Sendable>(_ operation: GraphQLRequest<T>) async throws -> GraphQLResponse<T> {
return try await execute(operation: operation)
}

/// Execute a GraphQL mutation
/// - Parameter operation: The GraphQL mutation operation
/// - Returns: The decoded response
func mutate<T: Decodable>(_ operation: GraphQLRequest<T>) async throws -> GraphQLResponse<T> {
func mutate<T: Decodable & Sendable>(_ operation: GraphQLRequest<T>) async throws -> GraphQLResponse<T> {
return try await execute(operation: operation)
}

/// Execute a raw GraphQL request
private func execute<T: Decodable>(
private func execute<T: Decodable & Sendable>(
operation: GraphQLRequest<T>
) async throws -> GraphQLResponse<T> {
let urlRequest = try getURLRequest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import Foundation
/// The country and language context for the API requests
/// see: https://shopify.dev/changelog/storefront-api-incontext-directive-supports-languages
@available(iOS 16.0, *)
struct InContextDirective {
struct InContextDirective: Sendable {
let countryCode: CountryCode
let languageCode: ShopifyLanguageCode

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/

/// GraphQL response structure
struct GraphQLResponse<T: Decodable>: Decodable {
struct GraphQLResponse<T: Decodable & Sendable>: Decodable, Sendable {
let data: T?
let errors: [GraphQLResponseError]?
let extensions: [String: AnyCodable]?
Expand All @@ -34,18 +34,18 @@ struct GraphQLResponse<T: Decodable>: Decodable {
}

/// GraphQL error from response
struct GraphQLResponseError: Decodable, Error {
struct GraphQLResponseError: Decodable, Error, Sendable {
let message: String
let path: [String]?
let locations: [Location]?
let extensions: Extensions?

struct Location: Decodable {
struct Location: Decodable, Sendable {
let line: Int
let column: Int
}

struct Extensions: Decodable {
struct Extensions: Decodable, Sendable {
let code: String?
let field: [String]?
let cost: Int?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import Foundation
/// Custom scalar types for the Storefront API
enum GraphQLScalars {
/// Represents a globally unique identifier (GID) in the Storefront API
struct ID: Codable, Hashable, CustomStringConvertible {
struct ID: Codable, Sendable, Hashable, CustomStringConvertible {
let rawValue: String

init(_ rawValue: String) {
Expand Down Expand Up @@ -54,13 +54,13 @@ enum GraphQLScalars {
}

/// Represents a monetary value with a decimal amount and currency code
struct Money: Codable, Hashable {
struct Money: Codable, Sendable, Hashable {
let amount: Decimal
let currencyCode: String
}

/// Represents an ISO 8601 encoded date-time string
struct DateTime: Codable, Hashable {
struct DateTime: Codable, Sendable, Hashable {
let date: Date

init(_ date: Date) {
Expand Down Expand Up @@ -99,7 +99,7 @@ enum GraphQLScalars {
}

/// Represents an absolute URL
struct URL: Codable, Hashable {
struct URL: Codable, Sendable, Hashable {
let url: Foundation.URL

init(_ url: Foundation.URL) {
Expand Down Expand Up @@ -130,7 +130,7 @@ enum GraphQLScalars {
}

/// Represents HTML content
struct HTML: Codable, Hashable {
struct HTML: Codable, Sendable, Hashable {
let rawValue: String

init(_ rawValue: String) {
Expand All @@ -150,7 +150,7 @@ enum GraphQLScalars {
}

/// ISO 4217 currency codes
enum CurrencyCode: String, Codable, CaseIterable {
enum CurrencyCode: String, Codable, Sendable, CaseIterable {
case aed = "AED"
case afn = "AFN"
case all = "ALL"
Expand Down Expand Up @@ -309,7 +309,7 @@ enum CurrencyCode: String, Codable, CaseIterable {
case zmw = "ZMW"
}

enum LanguageCode: String, CaseIterable, Codable {
enum LanguageCode: String, CaseIterable, Codable, Sendable {
/// Afrikaans
case AF
/// Akan
Expand Down Expand Up @@ -604,7 +604,7 @@ enum LanguageCode: String, CaseIterable, Codable {
/// If a territory doesn't have a country code value in the `CountryCode` enum, then it might be considered a subdivision
/// of another country. For example, the territories associated with Spain are represented by the country code `ES`,
/// and the territories associated with the United States of America are represented by the country code `US`.
enum CountryCode: String, CaseIterable, Codable {
enum CountryCode: String, CaseIterable, Codable, Sendable {
/// Afghanistan
case AF
/// Åland Islands
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import Foundation

/// GraphQL client errors
enum GraphQLError: LocalizedError {
enum GraphQLError: LocalizedError, Sendable {
case networkError(String)
case httpError(statusCode: Int, data: Data)
case decodingError(Error)
Expand Down Expand Up @@ -52,8 +52,8 @@ enum GraphQLError: LocalizedError {
}

/// Helper type for encoding/decoding Any values
struct AnyCodable: Codable {
let value: Any
struct AnyCodable: Codable, @unchecked Sendable {
nonisolated(unsafe) let value: Any

init(_ value: Any) {
self.value = value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ extension StorefrontAPI {
/// - Returns: The created cart
func cartCreate(
with items: [GraphQLScalars.ID] = [],
customer: ShopifyAcceleratedCheckouts.Customer? = nil
customer: ShopifyAcceleratedCheckouts.CustomerIdentity? = nil
) async throws -> Cart {
var input: [String: Any] = [
"lines": items.map { ["merchandiseId": $0.rawValue] }
Expand Down Expand Up @@ -75,7 +75,7 @@ extension StorefrontAPI {
return cart
}

struct CartBuyerIdentityUpdateInput: Codable {
struct CartBuyerIdentityUpdateInput: Codable, Sendable {
var email: String?
var phoneNumber: String?
var customerAccessToken: String?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ extension StorefrontAPI {
/// Get shop information
/// - Returns: Shop details
func shop() async throws -> Shop {
try await QueryCache.shared.load(
let client = client
return try await QueryCache.shared.load(
cacheKey: "shop",
url: client.url,
query: {
let response = try await self.client.query(Operations.getShop())
let response = try await client.query(Operations.getShop())
guard let shop = response.data?.shop else {
throw StorefrontAPI.Errors.payload(propertyName: "shop")
}
Expand All @@ -64,16 +65,16 @@ extension StorefrontAPI {
actor QueryCache {
static let shared = QueryCache()

private var cache: [String: Any] = [:]
private var inflightRequests: [String: Any] = [:]
private var cache: [String: any Sendable] = [:]
private var inflightRequests: [String: any Sendable] = [:]

private init() {}

/// Loads data with deduplication - multiple simultaneous calls will share the same request
func load<T>(
func load<T: Sendable>(
cacheKey: String,
url: URL,
query: @escaping () async throws -> T
query: @Sendable @escaping () async throws -> T
) async throws -> T {
let key = buildCacheKey(queryKey: cacheKey, url: url)

Expand Down Expand Up @@ -103,7 +104,7 @@ actor QueryCache {
}
}

private func cache(_ result: some Any, for key: String) {
private func cache(_ result: some Sendable, for key: String) {
cache[key] = result
}

Expand Down
Loading
Loading