Skip to content
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
94 changes: 59 additions & 35 deletions Sources/SwiftUIQuery/Core/QueryKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,85 @@

/// A protocol that represents a unique identifier for queries
/// Equivalent to TanStack Query's QueryKey (ReadonlyArray<unknown>)
public protocol QueryKey: Sendable, Hashable, Codable {
public protocol QueryKey: Sendable, Equatable {
/// Convert the query key to a string hash for identification
var queryHash: String { get }
}

/// Default QueryKey implementation using arrays of strings
public struct ArrayQueryKey: QueryKey {
public let components: [String]

public init(_ components: String...) {
self.components = components
extension QueryKey where Self: Hashable & Codable {
public var queryHash: String {
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .sortedKeys // stable output with key sorted
guard let jsonData = try? jsonEncoder.encode(self) else {
return "\(hashValue)"
}
return String(decoding: jsonData, as: UTF8.self)

Check failure on line 19 in Sources/SwiftUIQuery/Core/QueryKey.swift

View workflow job for this annotation

GitHub Actions / Swift Format

Optional Data -> String Conversion Violation: Prefer failable `String(bytes:encoding:)` initializer when converting `Data` to `String` (optional_data_string_conversion)
}
}

public init(_ components: [String]) {
self.components = components
}
// MARK: - QueryKey Extensions for Common Types

extension String: QueryKey {
public var queryHash: String {
// Create a deterministic hash similar to TanStack Query's approach
guard let jsonData = try? JSONEncoder().encode(components.sorted()),
let jsonString = String(data: jsonData, encoding: .utf8) else {
return components.sorted().joined(separator: "|")
}
return jsonString
self
}
}

/// Generic QueryKey implementation for any Codable type
public struct GenericQueryKey<T: Sendable & Codable & Hashable>: QueryKey {
public let value: T
extension Array: QueryKey where Element: Hashable & Codable {}
extension Dictionary: QueryKey where Key: Hashable & Codable, Value: Hashable & Codable {}

public init(_ value: T) {
self.value = value
public typealias QueryKeyCodable = Codable & Hashable & Sendable
public struct KeyTuple2<K1: QueryKeyCodable, K2: QueryKeyCodable>: QueryKey, QueryKeyCodable {
public let key1: K1
public let key2: K2

public init(_ key1: K1, _ key2: K2) {
self.key1 = key1
self.key2 = key2
}

public var queryHash: String {
guard let jsonData = try? JSONEncoder().encode(value),
let jsonString = String(data: jsonData, encoding: .utf8) else {
return String(describing: value)
}
return jsonString
public init(_ key1: (some Any).Type, _ key2: K2) where K1 == String {
self.key1 = String(describing: key1)
self.key2 = key2
}
}

// MARK: - QueryKey Extensions for Common Types
public struct KeyTuple3<K1: QueryKeyCodable, K2: QueryKeyCodable, K3: QueryKeyCodable>: QueryKey, QueryKeyCodable {
public let key1: K1
public let key2: K2
public let key3: K3

extension String: QueryKey {
public var queryHash: String {
self
public init(_ key1: K1, _ key2: K2, _ key3: K3) {
self.key1 = key1
self.key2 = key2
self.key3 = key3
}

public init(_ key1: (some Any).Type, _ key2: K2, _ key3: K3) where K1 == String {
self.key1 = String(describing: key1)
self.key2 = key2
self.key3 = key3
}
}

extension [String]: QueryKey {
public var queryHash: String {
// Create a deterministic hash by joining sorted components
sorted().joined(separator: "|")
public struct KeyTuple4<K1: QueryKeyCodable, K2: QueryKeyCodable, K3: QueryKeyCodable, K4: QueryKeyCodable>: QueryKey,
QueryKeyCodable {
public let key1: K1
public let key2: K2
public let key3: K3
public let key4: K4

public init(_ key1: K1, _ key2: K2, _ key3: K3, _ key4: K4) {
self.key1 = key1
self.key2 = key2
self.key3 = key3
self.key4 = key4
}

public init(_ key1: (some Any).Type, _ key2: K2, _ key3: K3, _ key4: K4) where K1 == String {
self.key1 = String(describing: key1)
self.key2 = key2
self.key3 = key3
self.key4 = key4
}
}
140 changes: 133 additions & 7 deletions Sources/SwiftUIQuery/UseInfiniteQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,9 @@ public struct UseInfiniteQuery<

/// Additional convenience methods for SwiftUI integration
extension UseInfiniteQuery {
/// Create a UseInfiniteQuery with string-based query key
/// Create UseInfiniteQuery with KeyTuple2-based query key
/// - Parameters:
/// - queryKey: String-based query key
/// - queryKey: KeyTuple2 identifier for the query
/// - queryFn: Function that fetches page data
/// - getNextPageParam: Function to get next page parameter from pages
/// - getPreviousPageParam: Function to determine the previous page parameter
Expand All @@ -323,9 +323,135 @@ extension UseInfiniteQuery {
/// - enabled: Whether the query should execute automatically (default: true)
/// - queryClient: Optional query client (uses shared instance if nil)
/// - content: View builder that receives the query result
public init(
queryKey: String,
queryFn: @escaping @Sendable (String, TPageParam?) async throws -> TData,
public init<K1: QueryKeyCodable, K2: QueryKeyCodable>(
queryKey: KeyTuple2<K1, K2>,
queryFn: @escaping @Sendable (KeyTuple2<K1, K2>, TPageParam?) async throws -> TData,
getNextPageParam: @escaping GetNextPageParamFunction<TData, TPageParam>,
getPreviousPageParam: GetPreviousPageParamFunction<TData, TPageParam>? = nil,
initialPageParam: TPageParam? = nil,
maxPages: Int? = nil,
retryConfig: RetryConfig = RetryConfig(),
networkMode: NetworkMode = .online,
staleTime: TimeInterval = 0,
gcTime: TimeInterval = defaultGcTime,
refetchTriggers: RefetchTriggers = .default,
refetchOnAppear: RefetchOnAppear = .ifStale,
structuralSharing: Bool = true,
meta: QueryMeta? = nil,
enabled: Bool = true,
queryClient: QueryClient? = nil,
@ViewBuilder content: @escaping (UseInfiniteQueryResult<TData, TPageParam>) -> Content
) where TKey == KeyTuple2<K1, K2> {
let options = InfiniteQueryOptions<TData, QueryError, KeyTuple2<K1, K2>, TPageParam>(
queryKey: queryKey,
queryFn: queryFn,
getNextPageParam: getNextPageParam,
getPreviousPageParam: getPreviousPageParam,
initialPageParam: initialPageParam,
maxPages: maxPages,
retryConfig: retryConfig,
networkMode: networkMode,
staleTime: staleTime,
gcTime: gcTime,
refetchTriggers: refetchTriggers,
refetchOnAppear: refetchOnAppear,
structuralSharing: structuralSharing,
meta: meta,
enabled: enabled
)

self.init(
options: options,
queryClient: queryClient,
content: content
)
}

/// Create UseInfiniteQuery with KeyTuple3-based query key
/// - Parameters:
/// - queryKey: KeyTuple3 identifier for the query
/// - queryFn: Function that fetches page data
/// - getNextPageParam: Function to get next page parameter from pages
/// - getPreviousPageParam: Function to determine the previous page parameter
/// - initialPageParam: Initial page parameter for the first page
/// - maxPages: Maximum number of pages to retain
/// - retryConfig: Configuration for retry behavior (default: RetryConfig())
/// - networkMode: Network behavior configuration (default: .online)
/// - staleTime: Time before data is considered stale (default: 0)
/// - gcTime: Time before unused data is garbage collected (default: 5 minutes)
/// - refetchTriggers: Configuration for automatic refetching triggers (default: .default)
/// - refetchOnAppear: When to refetch data on view appear (default: .ifStale)
/// - structuralSharing: Whether to use structural sharing for performance (default: true)
/// - meta: Arbitrary metadata for this query
/// - enabled: Whether the query should execute automatically (default: true)
/// - queryClient: Optional query client (uses shared instance if nil)
/// - content: View builder that receives the query result
public init<K1: QueryKeyCodable, K2: QueryKeyCodable, K3: QueryKeyCodable>(
queryKey: KeyTuple3<K1, K2, K3>,
queryFn: @escaping @Sendable (KeyTuple3<K1, K2, K3>, TPageParam?) async throws -> TData,
getNextPageParam: @escaping GetNextPageParamFunction<TData, TPageParam>,
getPreviousPageParam: GetPreviousPageParamFunction<TData, TPageParam>? = nil,
initialPageParam: TPageParam? = nil,
maxPages: Int? = nil,
retryConfig: RetryConfig = RetryConfig(),
networkMode: NetworkMode = .online,
staleTime: TimeInterval = 0,
gcTime: TimeInterval = defaultGcTime,
refetchTriggers: RefetchTriggers = .default,
refetchOnAppear: RefetchOnAppear = .ifStale,
structuralSharing: Bool = true,
meta: QueryMeta? = nil,
enabled: Bool = true,
queryClient: QueryClient? = nil,
@ViewBuilder content: @escaping (UseInfiniteQueryResult<TData, TPageParam>) -> Content
) where TKey == KeyTuple3<K1, K2, K3> {
let options = InfiniteQueryOptions<TData, QueryError, KeyTuple3<K1, K2, K3>, TPageParam>(
queryKey: queryKey,
queryFn: queryFn,
getNextPageParam: getNextPageParam,
getPreviousPageParam: getPreviousPageParam,
initialPageParam: initialPageParam,
maxPages: maxPages,
retryConfig: retryConfig,
networkMode: networkMode,
staleTime: staleTime,
gcTime: gcTime,
refetchTriggers: refetchTriggers,
refetchOnAppear: refetchOnAppear,
structuralSharing: structuralSharing,
meta: meta,
enabled: enabled
)

self.init(
options: options,
queryClient: queryClient,
content: content
)
}

/// Create UseInfiniteQuery with KeyTuple4-based query key
/// - Parameters:
/// - queryKey: KeyTuple4 identifier for the query
/// - queryFn: Function that fetches page data
/// - getNextPageParam: Function to get next page parameter from pages
/// - getPreviousPageParam: Function to determine the previous page parameter
/// - initialPageParam: Initial page parameter for the first page
/// - maxPages: Maximum number of pages to retain
/// - retryConfig: Configuration for retry behavior (default: RetryConfig())
/// - networkMode: Network behavior configuration (default: .online)
/// - staleTime: Time before data is considered stale (default: 0)
/// - gcTime: Time before unused data is garbage collected (default: 5 minutes)
/// - refetchTriggers: Configuration for automatic refetching triggers (default: .default)
/// - refetchOnAppear: When to refetch data on view appear (default: .ifStale)
/// - structuralSharing: Whether to use structural sharing for performance (default: true)
/// - meta: Arbitrary metadata for this query
/// - enabled: Whether the query should execute automatically (default: true)
/// - queryClient: Optional query client (uses shared instance if nil)
/// - content: View builder that receives the query result
public init<K1: QueryKeyCodable, K2: QueryKeyCodable, K3: QueryKeyCodable, K4: QueryKeyCodable>(
queryKey: KeyTuple4<K1, K2, K3, K4>,
queryFn: @escaping @Sendable (KeyTuple4<K1, K2, K3, K4>, TPageParam?) async throws -> TData,
getNextPageParam: @escaping GetNextPageParamFunction<TData, TPageParam>,
getPreviousPageParam: GetPreviousPageParamFunction<TData, TPageParam>? = nil,
initialPageParam: TPageParam? = nil,
Expand All @@ -341,8 +467,8 @@ extension UseInfiniteQuery {
enabled: Bool = true,
queryClient: QueryClient? = nil,
@ViewBuilder content: @escaping (UseInfiniteQueryResult<TData, TPageParam>) -> Content
) where TKey == String {
let options = InfiniteQueryOptions<TData, QueryError, String, TPageParam>(
) where TKey == KeyTuple4<K1, K2, K3, K4> {
let options = InfiniteQueryOptions<TData, QueryError, KeyTuple4<K1, K2, K3, K4>, TPageParam>(
queryKey: queryKey,
queryFn: queryFn,
getNextPageParam: getNextPageParam,
Expand Down
Loading
Loading